Spring Cloud微服务如何设计异常处理机制?

齐达内 发表了文章 • 0 个评论 • 162 次浏览 • 2019-06-03 21:36 • 来自相关话题

【编者的话】今天和大家聊一下在采用Spring Cloud进行微服务架构设计时,微服务之间调用时异常处理机制应该如何设计的问题。我们知道在进行微服务架构设计时,一个微服务一般来说不可避免地会同时面向内部和外部提供相应的功能服务接口。面向外部提供的服务接口,会通 ...查看全部
【编者的话】今天和大家聊一下在采用Spring Cloud进行微服务架构设计时,微服务之间调用时异常处理机制应该如何设计的问题。我们知道在进行微服务架构设计时,一个微服务一般来说不可避免地会同时面向内部和外部提供相应的功能服务接口。面向外部提供的服务接口,会通过服务网关(如使用Zuul提供的apiGateway)面向公网提供服务,如给App客户端提供的用户登陆、注册等服务接口。

而面向内部的服务接口,则是在进行微服务拆分后由于各个微服务系统的边界划定问题所导致的功能逻辑分散,而需要微服务之间彼此提供内部调用接口,从而实现一个完整的功能逻辑,它是之前单体应用中本地代码接口调用的服务化升级拆分。例如,需要在团购系统中,从下单到完成一次支付,需要交易系统在调用订单系统完成下单后再调用支付系统,从而完成一次团购下单流程,这个时候由于交易系统、订单系统及支付系统是三个不同的微服务,所以为了完成这次用户订单,需要App调用交易系统提供的外部下单接口后,由交易系统以内部服务调用的方式再调用订单系统和支付系统,以完成整个交易流程。如下图所示:
1.png

这里需要说明的是,在基于Spring Cloud的微服务架构中,所有服务都是通过如Consul或Eureka这样的服务中间件来实现的服务注册与发现后来进行服务调用的,只是面向外部的服务接口会通过网关服务进行暴露,面向内部的服务接口则在服务网关进行屏蔽,避免直接暴露给公网。而内部微服务间的调用还是可以直接通过Consul或Eureka进行服务发现调用,这二者并不冲突,只是外部客户端是通过调用服务网关,服务网关通过Consul再具体路由到对应的微服务接口,而内部微服务则是直接通过Consul或者Eureka发现服务后直接进行调用。如果你想和更多Spring Cloud技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态
#异常处理的差异

面向外部的服务接口,我们一般会将接口的报文形式以JSON的方式进行响应,除了正常的数据报文外,我们一般会在报文格式中冗余一个响应码和响应信息的字段,如正常的接口成功返回:
 {
"code" : "0",
"msg" : "success",
"data" : {
"userId" : "zhangsan",
"balance" : 5000
}
}

而如果出现异常或者错误,则会相应地返回错误码和错误信息,如:
 {
"code" : "-1",
"msg" : "请求参数错误",
"data" : null
}

在编写面向外部的服务接口时,服务端所有的异常处理我们都要进行相应地捕获,并在controller层映射成相应地错误码和错误信息,因为面向外部的是直接暴露给用户的,是需要进行比较友好的展示和提示的,即便系统出现了异常也要坚决向用户进行友好输出,千万不能输出代码级别的异常信息,否则用户会一头雾水。对于客户端而言,只需要按照约定的报文格式进行报文解析及逻辑处理即可,一般我们在开发中调用的第三方开放服务接口也都会进行类似的设计,错误码及错误信息分类得也是非常清晰!

而微服务间彼此的调用在异常处理方面,我们则是希望更直截了当一些,就像调用本地接口一样方便,在基于Spring Cloud的微服务体系中,微服务提供方会提供相应的客户端SDK代码,而客户端SDK代码则是通过FeignClient的方式进行服务调用,如:而微服务间彼此的调用在异常处理方面,我们则是希望更直截了当一些,就像调用本地接口一样方便,在基于Spring Cloud的微服务体系中,微服务提供方会提供相应的客户端SDK代码,而客户端SDK代码则是通过FeignClient的方式进行服务调用,如:
@FeignClient( value = "order", configuration = OrderClientConfiguration.class, fallback = OrderClientFallback.class )
public interface OrderClient {
/[i] 订单(内) [/i]/
@RequestMapping( value = "/order/createOrder", method = RequestMethod.POST )
OrderCostDetailVo orderCost( @RequestParam(value = "orderId") String orderId,
@RequestParam(value = "userId") long userId,
@RequestParam(value = "orderType") String orderType,
@RequestParam(value = "orderCost") int orderCost,
@RequestParam(value = "currency") String currency,
@RequestParam(value = "tradeTime") String tradeTime )
}

而服务的调用方在拿到这样的SDK后就可以忽略具体的调用细节,实现像本地接口一样调用其他微服务的内部接口了,当然这个是FeignClient框架提供的功能,它内部会集成像Ribbon和Hystrix这样的框架来实现客户端服务调用的负载均衡和服务熔断功能(注解上会指定熔断触发后的处理代码类),由于本文的主题是讨论异常处理,这里暂时就不作展开了。

现在的问题是,虽然FeignClient向服务调用方提供了类似于本地代码调用的服务对接体验,但服务调用方却是不希望调用时发生错误的,即便发生错误,如何进行错误处理也是服务调用方希望知道的事情。另一方面,我们在设计内部接口时,又不希望将报文形式搞得类似于外部接口那样复杂,因为大多数场景下,我们是希望服务的调用方可以直截了的获取到数据,从而直接利用FeignClient客户端的封装,将其转化为本地对象使用。
@Data
@Builder
public class OrderCostDetailVo implements Serializable {
private String orderId;
private String userId;
private int status; /[i] 1:欠费状态;2:扣费成功 [/i]/
private int orderCost;
private String currency;
private int payCost;
private int oweCost;
public OrderCostDetailVo( String orderId, String userId, int status, int orderCost, String currency, int payCost,
int oweCost )
{
this.orderId = orderId;
this.userId = userId;
this.status = status;
this.orderCost = orderCost;
this.currency = currency;
this.payCost = payCost;
this.oweCost = oweCost;
}
}

如我们在把返回数据就是设计成了一个正常的VO/BO对象的这种形式,而不是向外部接口那么样额外设计错误码或者错误信息之类的字段,当然,也并不是说那样的设计方式不可以,只是感觉会让内部正常的逻辑调用,变得比较啰嗦和冗余,毕竟对于内部微服务调用来说,要么对,要么错,错了就Fallback逻辑就好了。

不过,话虽说如此,可毕竟服务是不可避免的会有异常情况的。如果内部服务在调用时发生了错误,调用方还是应该知道具体的错误信息的,只是这种错误信息的提示需要以异常的方式被集成了FeignClient的服务调用方捕获,并且不影响正常逻辑下的返回对象设计,也就是说我不想额外在每个对象中都增加两个冗余的错误信息字段,因为这样看起来不是那么优雅!

既然如此,那么应该如何设计呢?
#最佳实践设计

首先,无论是内部还是外部的微服务,在服务端我们都应该设计一个全局异常处理类,用来统一封装系统在抛出异常时面向调用方的返回信息。而实现这样一个机制,我们可以利用Spring提供的注解@ControllerAdvice来实现异常的全局拦截和统一处理功能。如:
@Slf4j
@RestController
@ControllerAdvice
public class GlobalExceptionHandler {
@Resource
MessageSource messageSource;
@ExceptionHandler( { org.springframework.web.bind.MissingServletRequestParameterException.class } )
@ResponseBody
public APIResponse processRequestParameterException( HttpServletRequest request,
HttpServletResponse response,
MissingServletRequestParameterException e )
{
response.setStatus( HttpStatus.FORBIDDEN.value() );
response.setContentType( "application/json;charset=UTF-8" );
APIResponse result = new APIResponse();
result.setCode( ApiResultStatus.BAD_REQUEST.getApiResultStatus() );
result.setMessage(
messageSource.getMessage( ApiResultStatus.BAD_REQUEST.getMessageResourceName(),
null, LocaleContextHolder.getLocale() ) + e.getParameterName() );
return(result);
}

@ExceptionHandler( Exception.class )
@ResponseBody
public APIResponse processDefaultException( HttpServletResponse response,
Exception e )
{
/[i] log.error("Server exception", e); [/i]/
response.setStatus( HttpStatus.INTERNAL_SERVER_ERROR.value() );
response.setContentType( "application/json;charset=UTF-8" );
APIResponse result = new APIResponse();
result.setCode( ApiResultStatus.INTERNAL_SERVER_ERROR.getApiResultStatus() );
result.setMessage( messageSource.getMessage( ApiResultStatus.INTERNAL_SERVER_ERROR.getMessageResourceName(), null,
LocaleContextHolder.getLocale() ) );
return(result);
}

@ExceptionHandler( ApiException.class )
@ResponseBody
public APIResponse processApiException( HttpServletResponse response,
ApiException e )
{
APIResponse result = new APIResponse();
response.setStatus( e.getApiResultStatus().getHttpStatus() );
response.setContentType( "application/json;charset=UTF-8" );
result.setCode( e.getApiResultStatus().getApiResultStatus() );
String message = messageSource.getMessage( e.getApiResultStatus().getMessageResourceName(),
null, LocaleContextHolder.getLocale() );
result.setMessage( message );
/[i] log.error("Knowned exception", e.getMessage(), e); [/i]/
return(result);
}

/**
* 内部微服务异常统一处理方法
*/
@ExceptionHandler( InternalApiException.class )
@ResponseBody
public APIResponse processMicroServiceException( HttpServletResponse response,
InternalApiException e )
{
response.setStatus( HttpStatus.OK.value() );
response.setContentType( "application/json;charset=UTF-8" );
APIResponse result = new APIResponse();
result.setCode( e.getCode() );
result.setMessage( e.getMessage() );
return(result);
}
}

如上述代码,我们在全局异常中针对内部统一异常及外部统一异常分别作了全局处理,这样只要服务接口抛出了这样的异常就会被全局处理类进行拦截并统一处理错误的返回信息。

理论上我们可以在这个全局异常处理类中,捕获处理服务接口业务层抛出的所有异常并统一响应,只是那样会让全局异常处理类变得非常臃肿,所以从最佳实践上考虑,我们一般会为内部和外部接口分别设计一个统一面向调用方的异常对象,如外部统一接口异常我们叫ApiException,而内部统一接口异常叫InternalApiException。这样,我们就需要在面向外部的服务接口controller层中,将所有的业务异常转换为ApiException;而在面向内部服务的controller层中将所有的业务异常转化为InternalApiException。如:
@RequestMapping( value = "/creatOrder", method = RequestMethod.POST )
public OrderCostDetailVo orderCost(
@RequestParam(value = "orderId") String orderId,
@RequestParam(value = "userId") long userId,
@RequestParam(value = "orderType") String orderType,
@RequestParam(value = "orderCost") int orderCost,
@RequestParam(value = "currency") String currency,
@RequestParam(value = "tradeTime") String tradeTime ) throws InternalApiException
{
OrderCostVo costVo = OrderCostVo.builder().orderId( orderId ).userId( userId ).busiId( busiId ).orderType( orderType )
.duration( duration ).bikeType( bikeType ).bikeNo( bikeNo ).cityId( cityId ).orderCost( orderCost )
.currency( currency ).strategyId( strategyId ).tradeTime( tradeTime ).countryName( countryName )
.build();
OrderCostDetailVo orderCostDetailVo;
try {
orderCostDetailVo = orderCostServiceImpl.orderCost( costVo );
return(orderCostDetailVo);
} catch ( VerifyDataException e ) {
log.error( e.toString() );
throw new InternalApiException( e.getCode(), e.getMessage() );
} catch ( RepeatDeductException e ) {
log.error( e.toString() );
throw new InternalApiException( e.getCode(), e.getMessage() );
}
}

如上面的内部服务接口的controller层中将所有的业务异常类型都统一转换成了内部服务统一异常对象InternalApiException了。这样全局异常处理类,就可以针对这个异常进行统一响应处理了。

对于外部服务调用方的处理就不多说了。而对于内部服务调用方而言,为了能够更加优雅和方便地实现异常处理,我们也需要在基于FeignClient的SDK代码中抛出统一内部服务异常对象,如:
@FeignClient( value = "order", configuration = OrderClientConfiguration.class, fallback = OrderClientFallback.class )
public interface OrderClient {
/[i] 订单(内) [/i]/
@RequestMapping( value = "/order/createOrder", method = RequestMethod.POST )
OrderCostDetailVo orderCost( @RequestParam(value = "orderId") String orderId,
@RequestParam(value = "userId") long userId,
@RequestParam(value = "orderType") String orderType,
@RequestParam(value = "orderCost") int orderCost,
@RequestParam(value = "currency") String currency,
@RequestParam(value = "tradeTime") String tradeTime ) throws InternalApiException
};

这样在调用方进行调用时,就会强制要求调用方捕获这个异常,在正常情况下调用方不需要理会这个异常,像本地调用一样处理返回对象数据就可以了。在异常情况下,则会捕获到这个异常的信息,而这个异常信息则一般在服务端全局处理类中会被设计成一个带有错误码和错误信息的json数据,为了避免客户端额外编写这样的解析代码,FeignClient为我们提供了异常解码机制。如:
@Slf4j
@Configuration
public class FeignClientErrorDecoder implements feign.codec.ErrorDecoder {
private static final Gson gson = new Gson();
@Override
public Exception decode( String methodKey, Response response )
{
if ( response.status() != HttpStatus.OK.value() )
{
if ( response.status() == HttpStatus.SERVICE_UNAVAILABLE.value() )
{
String errorContent;
try {
errorContent = Util.toString( response.body().asReader() );
InternalApiException internalApiException = gson.fromJson( errorContent, InternalApiException.class );
return(internalApiException);
} catch ( IOException e ) {
log.error( "handle error exception" );
return(new InternalApiException( 500, "unknown error" ) );
}
}
}
return(new InternalApiException( 500, "unknown error" ) );
}
}

我们只需要在服务调用方增加这样一个FeignClient解码器,就可以在解码器中完成错误消息的转换。这样,我们在通过FeignClient调用微服务时就可以直接捕获到异常对象,从而实现向本地一样处理远程服务返回的异常对象了。

以上就是在利用Spring Cloud进行微服务拆分后关于异常处理机制的一点分享了,如有更好的方式,也欢迎大家给我留言!

作者:若丨寒
链接:https://www.jianshu.com/p/9fb7684bbeca

微服务化后缓存怎么做

大卫 发表了文章 • 0 个评论 • 210 次浏览 • 2019-06-03 15:41 • 来自相关话题

【编者的话】最近接手的代码中遇到几个缓存的问题,存在一些设计原则的问题,这里总结一下,希望可以对你有帮助。 #问题 问题1: 店铺数据的获取,将用户关注的数据放在店铺信息一起返回。 对外提供的接口: ...查看全部
【编者的话】最近接手的代码中遇到几个缓存的问题,存在一些设计原则的问题,这里总结一下,希望可以对你有帮助。
#问题

问题1: 店铺数据的获取,将用户关注的数据放在店铺信息一起返回。

对外提供的接口:
List getPageShop(final Query query,final Boolean cache);

返回的店铺信息:
public class Shop {

public static final long DEFAULT_PRIORITY = 10L;

/**
* 唯一标识
*/
private Long id;
//省略了店铺其他信息
/**
* 用户关注
*/
private ShopAttention attention;
}

当调用方设置cache为true时,因为有缓存的存在,获取不到用户是否关注的数据。

问题2: 统计店铺的被关注数导致的慢SQL,导致数据库cpu飙高,影响到了整个应用。

SQL:
SELECT shop_id, count(user_Id) as attentionNumber
FROM shop_attention
WHERE shop_id IN

#{shopId}

GROUP BY shopId

这两种代码的写法都是基于一个基准。

不同的地方的缓存策略不一样,比如我更新的地方,查找数据时不能缓存,页面展示的查找的地方需要缓存。 既然服务提供方不知道该不该缓存,那就不管了,交给调用方去管理。如果你想和更多微服务技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态

这种假设本身没什么问题,但是忽略了另外一个原则,服务的内聚性。不应该被外部知道的就没必要暴露给外部。

无论是面向过程的C,还是面向对象的语言,都强调内聚性,也就是高内聚,低耦合。单体应用中应当遵循这个原则,微服务同样遵循这个原则。但是在实际过程中,我们发现做到高内聚并不简单。我们必须要时时刻刻审视方法/服务的边界,只有确定好职责边界,才能写出高内聚的代码。
#问题分析

第一个问题,从缓存的角度来看,是忽略了数据的更新频繁性以及数据获取的不同场景。

对于店铺这样一个大的聚合根,本身包含的信息很多,有些数据可能会被频繁更改的,有些则会很少更新的。那么不同的修改频率,是否缓存/缓存策略自然不同,使用同一个参数Boolean cache来控制显然不妥

第二个问题,这种统计类的需求使用SQL统计是一种在数据量比较小的情况下的权宜之计,当数据规模增大后,必须要使用离线计算或者流式计算来解决。它本身是一个慢SQL,所以必须要控制号调用量,这种统计的数据量的时效性应该由服务方控制,不需要暴露给调用方。否则就会出现上述的问题,调用方并不清楚其中的逻辑,不走缓存的话就会使得调用次数增加,QPS的增加会导致慢SQL打垮数据库。
#解法

缓存更新本身就是一个难解的问题,在微服务化后,多个服务就更加复杂了。涉及到跨服务的多级缓存一致性的问题。

所以对大部分的业务,我们可以遵循这样的原则来简单有效处理。

对数据的有效性比较敏感的调用都收敛到服务内部(领域内部应该更合适),不要暴露给调用方。

领域内部做数据的缓存失效控制。

缓存预计算(有些页面的地方不希望首次打开慢)的逻辑也应该放在领域内控制,不要暴露给调用方。

在领域内部控制在不同的地方使用不同的缓存策略,比如更新数据的地方需要获取及时的数据。比如商品的价格,和商品的所属类目更新频次不同,需要有不同的过期时间。

跨服务调用为了减少rpc调用,可以再进行一层缓存。因为这些调用可以接受过期的数据,再进行一层缓存没问题,expired time叠加也没多大影响(expire time在这边主要是影响缓存的命中数)

以上述店铺查询问题改造为例
1.png

扩展:如果后续有case在跨服务的调用时,对数据的过期比较敏感,并且在调用方也做了缓存,那就是跨服务的多级缓存一致性的问题。那就需要服务方告知调用方缓存何时失效,使用消息队列or其他方式来实现。

作者:方丈的寺院
原文:https://fangzhang.blog.csdn.net/article/details/89892575

容器环境下Node.js的内存管理

老马 发表了文章 • 0 个评论 • 186 次浏览 • 2019-06-03 15:26 • 来自相关话题

【编者的话】在基于容器的Node.js应用程序中管理内存的最佳实践。 在docker容器中运行Node.js应用程序时,传统的内存参数调整并不总是按预期工作。本文我们将阐述在基于容器的Node.js应用程序内存参数调优中并不总是有效的 ...查看全部
【编者的话】在基于容器的Node.js应用程序中管理内存的最佳实践。

在docker容器中运行Node.js应用程序时,传统的内存参数调整并不总是按预期工作。本文我们将阐述在基于容器的Node.js应用程序内存参数调优中并不总是有效的原因,并提供了在容器环境中使用Node.js应用程序时可以遵循的建议和最佳实践。
#综述
当Node.js应用程序运行在设置了内存限制的容器中时(使用docker --memory选项或者系统中的其他任意标志),请使用--max-old-space-size选项以确保Node.js知道其内存限制并且设置其值小于容器限制。

当Node.js应用程序在容器内运行时,将Node.js应用程序的峰值内存值设置为容器的内存容量(假如容器内存可以调整的话)。

接下来让我们更详细地探讨一下。
#Docker内存限制
默认情况下,容器是没有资源限制的,可以使用系统(OS)允许的尽可能多的可用内存资源。但是docker 运行命令可以指定选项,用于设置容器可以使用的内存或CPU。

该docker-run命令如下所示:docker run --memory --interactive --tty bash。

参数介绍:

* x是以y为单位的内存
* y可以是b(字节),k(千字节),m(兆字节),g(千兆字节)

例如:docker run --memory 1000000b --interactive --tty bash将内存或CPU限制设置为1,000,000字节。

要检查容器内的内存限制(以字节为单位),请使用以下命令:
cat /sys/fs/cgroup/memory/memory.limit_in_bytes

接下来我们一起来看下设置了--max_old_space_size之后容器的各种表现。

“旧生代”是V8内存托管堆的公共堆部分(即JavaScript对象所在的位置),并且该--max-old-space-size标志控制其最大大小。有关更多信息,请参阅关于-max-old-space-size

通常,当应用程序使用的内存多于容器内存时,应用程序将终止。如果你想和更多容器技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态

以下示例应用程序以10毫秒的间隔插入记录到列表。这个快速的间隔使得堆无限制地增长,模拟内存泄漏。
'use strict';
const list = [];
setInterval(()=> {
const record = new MyRecord();
list.push(record);
},10);
function MyRecord() {
var x='hii';
this.name = x.repeat(10000000);
this.id = x.repeat(10000000);
this.account = x.repeat(10000000);
}
setInterval(()=> {
console.log(process.memoryUsage())
},100);

本文所有的示例程序都可以在我推入Docker Hub的Docker镜像中获得。你也可以拉取Docker镜像并运行程序。使用docker pull ravali1906/dockermemory来获取图像。

或者,你可以自己构建镜像,并使用内存限制运行镜像,如下所示:
docker run --memory 512m --interactive --tty ravali1906/dockermemory bash

ravali1906/dockermemory是镜像的名称。

接下来,运行内存大于容器限制的应用程序:
$ node --max_old_space_size=1024 test-fatal-error.js
{ rss: 550498304,
heapTotal: 1090719744,
heapUsed: 1030627104,
external: 8272 }
Killed

PS:

* --max_old_space_size 取M为单位的值
* process.memoryUsage() 以字节为单位输出内存使用情况

当内存使用率超过某个阈值时,应用程序终止。但这些阈值是多少?有什么限制?我们来看一下约束。
#在容器中设置了--max-old-space-size约束的预期结果
默认情况下,Node.js(适用于11.x版本及以下)在32位和64位平台上使用最大堆大小分别为700MB和1400MB。对于当前默认值,请参阅博客末尾参考文章。

因此,理论上,当设置--max-old-space-size内存限制大于容器内存时,期望应用程序应直接被OOM(Out Of Memory)终止。

实际上,这可能不会发生。
#在容器中设置了--max-old-space-size约束的实际结果
并非所有通过--max-old-space-size指定的内存的容量都可以提前分配给应用程序。

相反,为了响应不断增长的需求,JavaScript内存堆是逐渐增长的。

应用程序使用的实际内存(以JavaScript堆中的对象的形式)可以在process.memoryUsage()API中的heapUsed字段看到。

因此,现在修改后的期望是,如果实际堆大小(驻留对象大小)超过OOM-KILLER阈值(--memory容器中的标志),则容器终止应用程序。
实际上,这也可能不会发生。

当我在容器受限的环境下分析内存密集型Node.js应用程序时,我看到两种情况:

* OOM-KILLER在heapTotal和heapUsed的值都高于容器限制之后,隔一段很长的时间才执行。
* OOM-KILLER根本没有执行。

#容器环境中的Node.js相关行为解释
监控容器中运行应用程序的重要指标是驻留集大小(RSS-resident set size)。

它属于应用程序虚拟内存的一部分。

或者说,它代表应用程序被分配的内存的一部分。

更进一步说,它表示应用程序分配的内存中当前处于活动状态的部分。

并非应用程序中的所有已分配内存都属于活动状态,这是因为“分配的内存”只有在进程实际开始使用它时才会真实分配。另外,为了响应其他进程的内存需求,系统可能swap out当前进程中处于非活动或休眠状态的内存给其他进程,后续如果当前进程需要的时候通过swapped in重新分配回来。

RSS反映了应用程序的可用和活动的内存量。
#证明
##示例1.创建一个大小超过容器内存限制的空Buffer对象
以下buffer_example.js为往内存分配空Buffer对象的实例代码:
const buf = Buffer.alloc(+process.argv[2] [i] 1024 [/i] 1024)
console.log(Math.round(buf.length / (1024 * 1024)))
console.log(Math.round(process.memoryUsage().rss / (1024 * 1024)))

运行Docker镜像并限制其内存用量:
docker run --memory 1024m --interactive --tty ravali1906/dockermemory bash

运行该应用程序。你会看到以下内容:
$ node buffer_example 2000
2000
16

即使内存大于容器限制,应用程序也不会终止。这是因为分配的内存还未被完全访问。rss值非常低,并且没有超过容器内存限制。
##示例2.创建一个大小超过容器内存限制的并填满的Buffer对象
以下为往内存分配Buffer对象并填满值的实例代码:
const buf = Buffer.alloc(+process.argv[2] [i] 1024 [/i] 1024,'x')
console.log(Math.round(buf.length / (1024 * 1024)))
console.log(Math.round(process.memoryUsage().rss / (1024 * 1024)))

运行Docker镜像并限制其内存用量:
docker run --memory 1024m --interactive --tty ravali1906/dockermemory bash

运行该应用程序
$ node buffer_example_fill.js 2000
2000
984

即使在这里应用也没有被终止!为什么?当活动内存达到容器设置限制时,并且swap space还有空间时,一些旧内存片段将被推送到swap space并可供同一进程使用。默认情况下,docker分配的交换空间量等于通过--memory标志设置的内存限制。有了这种机制,这个进程几乎可以使用2GB内存 - 1GB活动内存和1GB交换空间。简而言之,由于内存的交换机制,rss仍然在容器强制限制范围内,并且应用程序能够持续运行。
##示例3.创建一个大小超过容器内存限制的空Buffer对象并且限制容器使用swap空间
const buf = Buffer.alloc(+process.argv[2] [i] 1024 [/i] 1024,'x')
console.log(Math.round(buf.length / (1024 * 1024)))
console.log(Math.round(process.memoryUsage().rss / (1024 * 1024)))

运行镜像时限制docker内存,交换空间和关闭匿名页面交换,如下所示:
docker run --memory 1024m --memory-swap=1024m --memory-swappiness=0 --interactive --tty ravali1906/dockermemory bash

$ node buffer_example_fill.js 2000
Killed

当--memory-swap的值等于--memory的值时,它表示容器不使用任何额外的交换空间。此外,默认情况下,容器的内核可以交换出一定比例的匿名页,因此将--memory-swappiness设置为0以禁用它。因此,由于容器内没有发生交换,rss超出了容器限制,在正确的时间终止了进程。
#总结和建议
当您运行Node.js应用程序并将其--max-old-space-size设置为大于容器限制时,看起来Node.js可能不会“尊重”容器强制限制。但正如您在上面的示例中看到的,原因是应用程序可能无法使用标志访问JavaScript堆集的全长。

请记住,当您使用的内存多于容器中可用的内存时,无法保证应用按期望行为方式运行。为什么?因为进程的活动内存(rss)受到许多因素的影响,这些因素超出了应用程序的控制范围,并且可能依赖于高负载和环境 - 例如工作负载本身,系统中的并发级别,操作系统调度程序,垃圾收集率等。此外,这些因素可以在运行之间发生变化。
#关于Node.js堆大小的建议(当你可以控制它,但不能控制容器大小时)

* 运行一个空的Node.js应用程序,并测量空转情况下rss的使用情况(我在Node.js v10.x版本得到它的值约为20 MB)。
* 由于Node.js在堆中具有其他内存区域(例如new_space,code_space等),因此假设其默认配置会占用额外的20 MB。如果更改其默认值,请相应地调整此值。
* 从容器中的可用内存中减去此值(40 MB),得到的值设置为JavaScript的旧生代大小,应该是一个相当安全的值。

#关于容器内存大小的建议(当你可以控制它,但不能控制Node.js内存时)

* 运行涵盖高峰工作负载的应用程序。
* 观察rss空间的增长。使用top命令和process.memoryUsage()API得到最高值。
* 如果容器中不存在其他活动进程,将此值用作容器的内存限制。该值上浮10%以上会更加安全。

#备注
如果在容器环境下运行,Node.js 12.x的堆内存限制根据当前可用内存进行配置,而不是使用默认值。对于设置了max_old_space_size的场景,上面的建议仍然适用。此外,了解相关限制可以让您更好地调整应用并发挥应用的性能,因为默认值是相对保守的。

有关更多信息,请参阅配置默认堆转储

作者:Make_a_decision
链接:https://juejin.im/post/5cef9efc6fb9a07ec56e5cc5

容器监控之kube-state-metrics

徐亚松 发表了文章 • 0 个评论 • 194 次浏览 • 2019-06-03 13:21 • 来自相关话题

概述 已经有了cadvisor、heapster、metric-server,几乎容器运行的所有指标都能拿到,但是下面这种情况却无能为力: * 我调度了多少个replicas?现在可用的有几个? ...查看全部
概述

已经有了cadvisor、heapster、metric-server,几乎容器运行的所有指标都能拿到,但是下面这种情况却无能为力:

* 我调度了多少个replicas?现在可用的有几个?
* 多少个Pod是running/stopped/terminated状态?
* Pod重启了多少次?
* 我有多少job在运行中

而这些则是kube-state-metrics提供的内容,它基于client-go开发,轮询Kubernetes API,并将Kubernetes的结构化信息转换为metrics。

功能

kube-state-metrics提供的指标,按照阶段分为三种类别:

  • 1.实验性质的:k8s api中alpha阶段的或者spec的字段。
  • 2.稳定版本的:k8s中不向后兼容的主要版本的更新
  • 3.被废弃的:已经不在维护的。
指标类别包括:* CronJob Metrics* DaemonSet Metrics* Deployment Metrics* Job Metrics* LimitRange Metrics* Node Metrics* PersistentVolume Metrics* PersistentVolumeClaim Metrics* Pod Metrics* Pod Disruption Budget Metrics* ReplicaSet Metrics* ReplicationController Metrics* ResourceQuota Metrics* Service Metrics* StatefulSet Metrics* Namespace Metrics* Horizontal Pod Autoscaler Metrics* Endpoint Metrics* Secret Metrics* ConfigMap Metrics以pod为例:* kube_pod_info* kube_pod_owner* kube_pod_status_phase* kube_pod_status_ready* kube_pod_status_scheduled* kube_pod_container_status_waiting* kube_pod_container_status_terminated_reason* ... 使用部署清单
 kube-state-metrics/    ├── kube-state-metrics-cluster-role-binding.yaml    ├── kube-state-metrics-cluster-role.yaml    ├── kube-state-metrics-deployment.yaml    ├── kube-state-metrics-role-binding.yaml    ├── kube-state-metrics-role.yaml    ├── kube-state-metrics-service-account.yaml    ├── kube-state-metrics-service.yaml
主要镜像有:image: quay.io/coreos/kube-state-metrics:v1.5.0image: k8s.gcr.io/addon-resizer:1.8.3(参考metric-server文章,用于扩缩容)对于pod的资源限制,一般情况下:`200MiB memory0.1 cores`超过100节点的集群:`2MiB memory per node0.001 cores per node`kube-state-metrics做过一次性能优化,具体内容参考下文部署成功后,prometheus的target会出现如下标志
1.png
因为kube-state-metrics-service.yaml中有`prometheus.io/scrape: 'true'`标识,因此会将metric暴露给prometheus,而Prometheus会在kubernetes-service-endpoints这个job下自动发现kube-state-metrics,并开始拉取metrics,无需其他配置。如果你想和更多监控技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态。使用kube-state-metrics后的常用场景有:存在执行失败的Job: kube_job_status_failed{job="kubernetes-service-endpoints",k8s_app="kube-state-metrics"}==1
  • 集群节点状态错误: kube_node_status_condition{condition="Ready",status!="true"}==1
  • 集群中存在启动失败的Pod:kube_pod_status_phase{phase=~"Failed|Unknown"}==1
  • 最近30分钟内有Pod容器重启: changes(kube_pod_container_status_restarts[30m])>0
配合报警可以更好地监控集群的运行 与metric-server的对比* metric-server(或heapster)是从api-server中获取cpu、内存使用率这种监控指标,并把他们发送给存储后端,如influxdb或云厂商,他当前的核心作用是:为HPA等组件提供决策指标支持。
  • kube-state-metrics关注于获取k8s各种资源的最新状态,如deployment或者daemonset,之所以没有把kube-state-metrics纳入到metric-server的能力中,是因为他们的关注点本质上是不一样的。metric-server仅仅是获取、格式化现有数据,写入特定的存储,实质上是一个监控系统。而kube-state-metrics是将k8s的运行状况在内存中做了个快照,并且获取新的指标,但他没有能力导出这些指标
  • 换个角度讲,kube-state-metrics本身是metric-server的一种数据来源,虽然现在没有这么做。
  • 另外,像Prometheus这种监控系统,并不会去用metric-server中的数据,他都是自己做指标收集、集成的(Prometheus包含了metric-server的能力),但Prometheus可以监控metric-server本身组件的监控状态并适时报警,这里的监控就可以通过kube-state-metrics来实现,如metric-serverpod的运行状态。
深入解析kube-state-metrics本质上是不断轮询api-server,代码结构也很简单主要代码目录
.├── collectors│   ├── builder.go│   ├── collectors.go│   ├── configmap.go│   ......│   ├── testutils.go│   ├── testutils_test.go│   └── utils.go├── constant│   └── resource_unit.go├── metrics│   ├── metrics.go│   └── metrics_test.go├── metrics_store│   ├── metrics_store.go│   └── metrics_store_test.go├── options│   ├── collector.go│   ├── options.go│   ├── options_test.go│   ├── types.go│   └── types_test.go├── version│   └── version.go└── whiteblacklist    ├── whiteblacklist.go    └── whiteblacklist_test.go
所有类型:
var (	DefaultNamespaces = NamespaceList{metav1.NamespaceAll}	DefaultCollectors = CollectorSet{		"daemonsets":               struct{}{},		"deployments":              struct{}{},		"limitranges":              struct{}{},		"nodes":                    struct{}{},		"pods":                     struct{}{},		"poddisruptionbudgets":     struct{}{},		"replicasets":              struct{}{},		"replicationcontrollers":   struct{}{},		"resourcequotas":           struct{}{},		"services":                 struct{}{},		"jobs":                     struct{}{},		"cronjobs":                 struct{}{},		"statefulsets":             struct{}{},		"persistentvolumes":        struct{}{},		"persistentvolumeclaims":   struct{}{},		"namespaces":               struct{}{},		"horizontalpodautoscalers": struct{}{},		"endpoints":                struct{}{},		"secrets":                  struct{}{},		"configmaps":               struct{}{},	})
构建对应的收集器Family即一个类型的资源集合,如job下的kube_job_info、kube_job_created,都是一个FamilyGenerator实例
metrics.FamilyGenerator{			Name: "kube_job_info",			Type: metrics.MetricTypeGauge,			Help: "Information about job.",			GenerateFunc: wrapJobFunc(func(j *v1batch.Job) metrics.Family {				return metrics.Family{&metrics.Metric{					Name:  "kube_job_info",					Value: 1,				}}			}),		},
func (b [i]Builder) buildCronJobCollector() [/i]Collector {   // 过滤传入的白名单	filteredMetricFamilies := filterMetricFamilies(b.whiteBlackList, cronJobMetricFamilies)	composedMetricGenFuncs := composeMetricGenFuncs(filteredMetricFamilies)  // 将参数写到header中	familyHeaders := extractMetricFamilyHeaders(filteredMetricFamilies)  // NewMetricsStore实现了client-go的cache.Store接口,实现本地缓存。	store := metricsstore.NewMetricsStore(		familyHeaders,		composedMetricGenFuncs,	)  // 按namespace构建Reflector,监听变化	reflectorPerNamespace(b.ctx, b.kubeClient, &batchv1beta1.CronJob{}, store, b.namespaces, createCronJobListWatch)	return NewCollector(store)}
性能优化:kube-state-metrics在之前的版本中暴露出两个问题:
  • 1. /metrics接口响应慢(10-20s)
  • 2. 内存消耗太大,导致超出limit被杀掉
问题一的方案就是基于client-go的cache tool实现本地缓存,具体结构为:`var cache = map[uuid][]byte{}`问题二的的方案是:对于时间序列的字符串,是存在很多重复字符的(如namespace等前缀筛选),可以用指针或者结构化这些重复字符。 优化点和问题
  • 1.因为kube-state-metrics是监听资源的add、delete、update事件,那么在kube-state-metrics部署之前已经运行的资源,岂不是拿不到数据?kube-state-metric利用client-go可以初始化所有已经存在的资源对象,确保没有任何遗漏
  • 2.kube-state-metrics当前不会输出metadata信息(如help和description)
  • 3.缓存实现是基于golang的map,解决并发读问题当期是用了一个简单的互斥锁,应该可以解决问题,后续会考虑golang的sync.Map安全map。
  • 4.kube-state-metrics通过比较resource version来保证event的顺序
  • 5.kube-state-metrics并不保证包含所有资源

监控数据展示

基于kube-state-metrics的监控数据,可以组装一些常用的监控面板,如下面的grafana面板:
2.png

3.png

本文为容器监控实践系列文章,完整内容见:container-monitor-book

闲聊我心中的运维开发

aoxiang 发表了文章 • 0 个评论 • 232 次浏览 • 2019-06-03 12:21 • 来自相关话题

#前言 在我入职上家公司的运维部之前,我所以为的运维工程师只是修修电脑,拉拉网线,布布机器。 诸不知,运维所涉及的知识面、专业点非常广,对从业人员素质也要求非常高,运维工作在大型互联网公司的重要性不比业务开发差。且分类繁多 ...查看全部
#前言
在我入职上家公司的运维部之前,我所以为的运维工程师只是修修电脑,拉拉网线,布布机器。

诸不知,运维所涉及的知识面、专业点非常广,对从业人员素质也要求非常高,运维工作在大型互联网公司的重要性不比业务开发差。且分类繁多:

* 桌面运维工程师
* 业务运维工程师
* DBA工程师
* 配置工程师
* 运维开发工程师
* 以及其它....

1.png

原本准备写篇前端眼中的运维开发,恰巧前组长写了两篇结合自身六七年开发经验写的体会。用他的文章来阐述再合适不过了。以下来自其投稿以及穿插一些知识普及。
#DevOps:打破协作壁垒
来自维基百科:
2.png

DevOps(Development和Operations的组合詞)是一种重视「软件开发人员(Dev)」和「IT运维技术人员(Ops)」之间沟通合作的文化、运动或慣例。如果你想和更多DevOps技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态

透过自动化「软件交付」和「架构变更」的流程,来使得构建、测试、发布软件能够更加地快捷、频繁和可靠。

传统的软件组织将开发、IT运营和质量保障设为各自分离的部门,在这种环境下如何采用新的开发方法(例如敏捷软件开发),是一个重要的课题。

按照从前的工作方式,开发和部署,不需要IT支持或者QA深入的跨部门的支持;

而现在却需要极其紧密的多部门协作。而DevOps考虑的还不止是软件部署,它是一套针对这几个部门间沟通与协作问题的流程和方法。
3.png

具体来说,就是在 软件交付和部署过程中提高沟通与协作的效率,旨在更快、更可靠的的发布更高质量的产品。
#运维开发的价值
从岗位职责来看,运维开发要做的工作是:

通过开发技能帮助运维实现运维工作的自动化。说白了就是“辅助”,或者说是运维的臂膀,需要把运维中遇到的问题提供平台查询,或者把一些常见的重复操作给抽象出来做成工具,减少运维的人工介入。
4.png

运维服务伴随并支撑着业务发展的整个生命周期。

而DevOps将运维服务的执行方式升级为更加软件工程化的手段,减少人肉操作,DevOps 强调自动化、拉动式来提高团队交付效率与质量。

而传统的运维需要谋求技术转型,从原来只关注操作系统层面的技术已经不够了,还要增加对程序代码的性能调优、持续交付、容器化等软件基础架构方面的技能提升,也需要持续关注整个业务、应用、服务的生命周期管理。

简单来说,就是把过去传统的黑盒运维的思维方式抛弃,进入白盒运维的时代,我们必须更加深入代码、深入业务运营,让整个线上服务运行于更优质高效的状态。
#运维开发是什么?
要建设运维自动化或者实践 DevOps 离不开运维开发工程师的参与,但要怎样才能更好地发挥运维开发的作用呢?

我曾作为运维开发经理的角色和各种类型的运维开发一起协作过,团队中有本来就做运维开发的,也有本来做其他业务(电商、平台)的开发转来协助运维团队的,还有原本是做业务运维后来转型做运维开发的。
和他们协作一段日子后,总体感觉如下:

运维开发首先是一个程序员,不是运维工程师。

一个好的运维开发需要具备 「运维理解」+「开发能力」:

* 对「开发能力」的技术要求低于其他业务形态(如游戏、电商、搜索等)。
* 对运维业务的理解难度会低于电商、游戏等业务形态,即对「运维理解」的要求不高。
* 对运维相关技术栈的掌握程度要求高,如Python/PHP/Go/Shell、 Linux、Git、Nginx、Zabbix、Docker、Kubernetes等。

5.png

综上所述,运维开发是一个深度不算太深的职业分支,而现在之所以对运维开发需求量热起来了,主要由于老一辈的资深运维普遍研发能力有限,而这是有历史原因的。等到业界提出 DevOps的时候,他们往往已经专注于团队管理、容量规划、架构调优、运维服务质量等高级范畴,所以基本不太可能抽出大块的时间来重新学习编码并开发自动化系统。

所以,当我们有自动化系统的建设需求时,需要更专业的程序员来协助。但一般的非专职运维开发的程序员做出来的系统对于运维来说往往不太好使,这时候有部分年轻的运维工程师升级了研发技能,转型运维开发,把好使的运维系统做出来了,赢得了运维团队的好评,大家都为「运维开发」点赞。

所以,大家将 「好使的运维系统」 和 「运维开发」 等价起来,以为我们只要招来一个运维开发,那么一套完美的运维平台就能自动诞生出来,这是个很大的误区。
#打造「好使的DevOps系统」
其实「好使的DevOps系统」真正等价于「运维理解」+「开发能力」,这两种能力也是可以分离的,不一定要强加在运维开发工程师一个人的身上。

类似其他业务形态的开发过程,需要产品经理和程序员两种角色分离,企业也不会说要招聘既会写代码、又会出需求的程序员。

所以,当运维能把运维自动化的需求细致地文档化下来,把自动化系统的设计、架构等关键环节确立下来,这就是最好的「运维理解」。这时把这份靠谱、好使、细致的需求文档交给具备强「开发能力」的程序员,最终就可以得到「好使的运维系统」。

当然, 一般企业不会专门为运维开发配备「产品经理」,所以运维开发想要再往高级发展的话,也可以替代运维出需求,升级为运维产品经理,以程序员的思维角度来解决运维服务的工程效率和质量问题,我认为这也是类似 Google 所提倡的 SRE 文化。
##DevOps平台
编者补充描述。

光说不练假把戏,编者在上家公司的主职就是将DevOps操作界面化。
其中的核心模块:应用部署发布监控。

图为DevOps应用部署发布监控界面图:
6.png

我们组在做上图的DevOps系统时,面临的情况是:无产品、无设计、需求也是靠业务运维和开发们的口头描述。

其中的核心功能:应用部署界面,在参考其它同类产品后,发现都不适合业务场景,要么功能太分散,要么就仅是流程控制。于是前端功能里,我们做了这些:

* 区分不同环境下的包,实现有序管理。
* 应用的状态可以通过界面做启停、查看配置等任务。
* Jenkins服务操作可通过界面完成,简化配置工程师的工作。
* 业务运维与开发团队的日常发包工作界面化。

此时一个优秀的运维开发需具备以下技能:产品规划、产品设计、面向对象、需求模型、领域模型、设计模型、设计原则、设计模式、产品工具和文档能力等。

所以,当运维需求被理解、分析得足够透彻,以及运维开发获得了「产品经理」能力后,运维开发就是一种普通的开发分支,按需求文档编码即可。
#优秀的运维开发
从事DevOps平台开发相关工作已有六七年了,自身经历总结,觉得一个优秀的运维开发工程师应当具备以下能力和素质。
##提高运维意识。
从下到上,从上到下的工作都要做好,对上运维工作的价值和含金量可以得到认可,对下我们的工作能够提高效率解放运维。

运维意识是很重要,并不是你技术很牛,学的技术很多很熟,就不代表你不需要运维意识。

其实领导很看重运维意识的,例如有没有做好备份,权限分配问题,平台测试情况,故障响应时间等,这些都是意识,而不是你学了很多技术自认大牛了,平台发现故障你又没什么大不子,以为很简单的问题喜欢处理就处理,不需要向其它部门反馈等,领导不是看你的技术如何,而是看你的运维意识如何,你没运维意识,技术再牛也没用,只会让其它部门的人跟你不协调。
##了解业务场景
DevOps平台最终服务于运维部和开发测试部同事,因此只有熟悉了解了每一项业务的运维场景,才能更好去设计功能与代码开发,熟悉业务场景才能方方面面考虑周全,开发出来的代码才能满足各类场景应用。
##拒绝重复犯错
人难免会犯错,这是无法避免的,我们应当根据已有的犯错经验,总结犯错的原因,以及如何避免同类情况再次发生,甚至可以把一些典型的错误在团队中分享,把一个人的错误得到的经验传播于整个团队。
##凡事有备份,可回退
运维工作中经常有一些发布,迁移,备份等复杂操作,因此,在研发DevOPs平台的时候,要做好全面的操作计划,思考每一步可能的回退与备份。
##平台操作尽量简化
DevOps平台目的就是为了能够提高运维的工作效率,解放运维,因此在设计与开发的时候,应当保持操作简单,不要让事情变得太复杂,能点一下到位的,尽量不要让人点五六下才能完成操作。
##注重优化用户体验
DevOps开发是一个迭代的过程,虽然我们常说以功能开发为主,但是用户体验也同等重要,试想一下,纵使你开发的功能众多,如果体验不友好,用户便失去了再次使用的欲望,如果用户拒绝,抵触使用平台,做再多功能也是失败,最终平台推广失败。因此,在研发的过程中,我们应当深入体验自己开发的产品,把自己当成用户去体验平台操作。尽可能的去优化用户体验。这是一个优秀的运维开发工程师必需要懂的。

在设计与开发的过程中经常会碰到复杂,繁琐的场景,这个时候我们很容易失去耐心,我们要时刻提醒自己,必须严格履行自己的工作职责,端正自己的工作态度,做一件事,要么不做,既然做了就要做好:当你想要放弃的时候,想想当初为什么要开始。
#总结
本文是我个人对运维开发以及其职业发展的一些浅薄理解,总的来说,运维开发还是一个比较有意思且有良好发展的职业分支,虽然偶尔也要背黑锅,但也欢迎更多努力、聪明、有才华的同学加入运维开发行业。

链接:https://juejin.im/post/5cf29a6ae51d45778f076cd2

微服务间的调用和应用内调用有什么区别

阿娇 发表了文章 • 0 个评论 • 190 次浏览 • 2019-06-03 11:19 • 来自相关话题

2019企业IT现状和趋势调研报告:70.7%的企业有云原生相关计划

灵雀云 发表了文章 • 0 个评论 • 155 次浏览 • 2019-06-03 11:00 • 来自相关话题

2019年第一季度,灵雀云发起了“企业IT应用现状和云原生技术落地情况”的调研,通过定向邀请,3个月内共收集了400余份有效调研问卷,这些调研问卷80%以上都来自于国内政府、金融、能源、制造、汽车等传统行业的IT从业者。 发起本次调研 ...查看全部
2019年第一季度,灵雀云发起了“企业IT应用现状和云原生技术落地情况”的调研,通过定向邀请,3个月内共收集了400余份有效调研问卷,这些调研问卷80%以上都来自于国内政府、金融、能源、制造、汽车等传统行业的IT从业者。

发起本次调研的初衷,是我们希望了解当前企业,尤其是传统企业目前IT应用开发现状、以及以DevOps、Kubernetes、微服务等为代表的云原生技术在企业的应用情况,从而勾勒出传统行业IT发展趋势,并对于判断国内用户对云原生相关技术的认知度提供一个有价值的参考。
核心要点解读:
1、 约70%的参与调研者所在企业2019年IT预算有上浮;

2、 24.4%的参与调研者表示公司IT系统基本全靠自研,企业开始自建软件研发团队,主导IT应用的研发;

3、 70.7%的参与调研者所在企业表示在2019年有容器、DevOps和微服务方面的规划;

4、 11.4%的参与调研者所在企业已经试点了具有标杆意义的云原生实践,如精英团队的DevOps实践,小范围非核心应用的微服务拆分改造实践等。


pic2.jpg



本次调研的400多位调研对象中,80%以上来自金融、能源、制造、汽车等传统行业,其中17.3%来自基础架构部门, 22.5%来自运维部门,34.1%来自研发部门,还有约10%的被调研对象为企业的CIO/CTO等高级IT管理者。


pic3.jpg



被调研企业中,服务器规模在100-500台的比例为26.8%,500-1000台的企业占比22%,1000台服务器以上规模的14.6%。



IT系统自研还是外包



pic4.jpg




在数字化转型的背景下,传统外包的做法在被逐渐改变。在此次调查中,70.7%的参与调研者表示目前IT系统是自研外包兼而有之,其中核心业务系统以自己开发为主,24.4%的参与调研者表示公司IT系统基本全靠自研,只有4.9%的参与调研者选择了纯外包选项。这表明,企业开始不再将大部分业务系统,尤其是核心业务需求开发外包,开始自建软件研发团队,主导IT应用的研发。只有企业自己主导IT研发,才能够打造IT核心竞争力。

软件能力成为企业的核心竞争力,这恰好是数字化转型的本质之一。何谓成功的数字化转型?灵雀云认为,有三大衡量标志:IT部门由成本中心转为收入中心;企业自己主导IT产品的研发;改进工具、流程、文化来提高交付速度和质量。最终,实现客户满意度的提升、打造差异化竞争优势、加速产品上市。



IT系统更新频率




PIC5.jpg



在IT系统更新频率方面,每月都要更新、升级的比例达到了51.2%的高占比。同时,每3-6个月更新一次的比例达22%。每个传统领域,都受到了来自Fintech金融科技、车联网、物联网、新零售等新技术驱动的创新业务的挑战,传统企业只有借助IT手段才能实现持续发展,在速度和规模上保持竞争力。



IT系统和研发团队TOP 3挑战



pic6.jpg




本次参与调研的企业以中大型企业为主,其中研发团队规模达到100人以上的比例高达44.3%,20-100人规模的占32.4%。

PIC7.jpg




今天,许多企业都经过了大量IT建设,从分散到集中,造成IT系统越来越复杂,信息孤岛林立,架构臃肿等问题突出。调研中企业IT系统支撑所面临的压力位列前三的挑战分别是:系统复杂性越来越高(65.9%);应用交付压力大,交付速度无法满足业务需求(61.4%);运维管理复杂度提升,IT部门很难构建一支全功能团队(53.7%)。


PIC8.jpg



同时,研发团队所面临的挑战前三甲分别是:部署和运维复杂,运维成本高(74.6%);研发、测试、运维等角色之间相互孤立(62.3%);升级和变更流程复杂,IT服务和应用交付周期长(45.7%)。此外,比较突出的挑战还有,工具链无法完整集成,工具使用困难(32.3%),单体应用过于庞大,迭代效率低下(20.4%)。


pic9.jpg



上述结果充分表明,面对高度创新、快速变化和充满不确定性的新型业务需求,传统开发模式和IT架构已经成为掣肘。70.7%的参与调研企业表示2019年有容器、DevOps和微服务方面的规划和实施计划。

只有朝着持续交付、敏捷部署、快速迭代,通过敏捷IT赋予业务足够的敏捷,才能够满足不断变化的业务需求,重塑自身的生产力,形成竞争优势,带来更好的用户体验,这最终落到以Kubernetes/容器、DevOps和微服务为核心的云原生技术的落地上。云原生架构和理念与数字化转型一脉相承,帮助企业更加顺畅地实施数字化转型。



业务上云需求最强烈,开源、数字化转型受追捧



PIC10.jpg




在企业最关注的新兴技术趋势方面,云计算占比82.9%,企业将业务上云,提升IT资源效率作为首要关注对象。大数据和人工智能紧随其后,占比分别为73.2%和46.3%。其中开源解决方案在调研对象中的关注程度达到24.4%。

当前开源技术正在进入快速发展阶段,向着企业应用的方方面面深入。开源及开源社区不断将新的工具、方法和最佳实践用于云原生的实际业务用例,解决云原生用户的关键问题。借助许多开源解决方案,云原生部署的复杂性和难度也在得以降低。

此外,数字化转型的关注度为33.6%。如今每位IT从业者言必称数字化转型,IT能力也直接指向助力数字化转型。CIO和其他IT管理者已将企业的数字化计划置于新的高度,希望通过数字化来改变企业的商业和业务模式,数字化业务将从初步试验走向大规模应用。伴随企业数字化业务的不断成熟,预计未来几年,数字化转型将进入爆发阶段。



传统企业2019年IT预算稳中有升


PIC11.jpg





本次调研中,被调研企业今年IT工作的重点包括业务上云(56.1%),云原生、大数据、人工智能等新技术采用(53.7%),打造数字化团队,引领企业的数字化创新(43.9%),选择传统业务应用的比例不足20%。越来越多的企业将工作负载放在云端,将正在开发的应用或服务托管在云平台上,云市场不断增长。


PIC12.jpg



在IT预算方面,比客观经济形势略显乐观,和2018年IT预算相比,接近70%参与调研企业2019年的IT预算略有上浮,其中增长5%以内的企业占比37.5%,增长5-10%的企业占比21.2%,增长10%以上的企业达到12.7%。

此外,调研结果显示,数字化转型是一项需要通盘考虑的工作,需要项目管理部门、技术管理部门、开发部门、运维部门共同参与,制定统一的数字化转型方案和决策并推进。有些参与调研的企业特别强调2018年已经在全公司范围内试点了具有标杆意义的云原生实践,如精英团队的DevOps实践,小范围非核心应用的微服务拆分改造实践等,并且这些都将在2019年进行大范围推广。

容器云未来:Kubernetes、Istio 和 Knative

博云BoCloud 发表了文章 • 0 个评论 • 187 次浏览 • 2019-06-03 10:38 • 来自相关话题

导读 目前以Kubernetes为基础构建的容器生态逐渐完善,这其中Kubernetes、Istio、Knative三个独立项目被越来越多的人提及,并且已经开始尝试大规模落地实践,它们恰好构成了容器云的未来拼图。今天与大家一起分享下,这三个项目究 ...查看全部
导读
目前以Kubernetes为基础构建的容器生态逐渐完善,这其中Kubernetes、Istio、Knative三个独立项目被越来越多的人提及,并且已经开始尝试大规模落地实践,它们恰好构成了容器云的未来拼图。今天与大家一起分享下,这三个项目究竟解决了什么问题,为什么它们能够一鸣惊人。


随着微服务理念不断深入人心,越来越多的企业把自己的应用逐步由单体转变成微服务架构,Container容器技术的出现恰恰加速了这个转移过程,因为它有效地解决了N多服务的快速部署问题。但是随着服务数目的增多,越来越多的企业希望能够把相关服务有效地“聚合”在一起,方便统一部署与管理。Kubenretes的出现恰恰解决了大规模微服务编排部署所带来的挑战,让整个行业意识到PaaS的落地可以成为现实。

当随着微服务体系下的服务数目越来越多,服务运维成为必然要解决的问题,于是Istio出现了,基于网络代理与控制相分离的实现策略,允许对服务控制策略进行有效合理的管控。如果你想和更多容器技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态

到这里似乎到了很美好的阶段:

微服务:解决应用内聚、臃肿的问题。
Container:解决服务运行环境统一,和部署问题。
Kubernetes:解决大量微服务有效“聚合”部署问题。
Istio:解决服务上线面临的一系列治理问题。

这个阶段乍一看来,构建容器云似乎有了一个完整的链路和解决方式,一切都将变得那么“完美”。

现在让我们回过头来深入分析一下,微服务体系下的服务交互,目前是否存在问题。

首先,无论是http,还是rpc,本质上都是服务与服务的远程调用。开发应用程序中,无法做到服务与服务间的彼此透明。这样会导致一个问题:无论微服务业务拆分多么“精细”,本质上业务单元之间还是不能够独立运行和发展。同时在面向不同开发领域的衍生,无法选择最合适的实现方式。因此我们希望能够基于不同的“模板”+“配置”的方式能够把开发环境标准化处理,同时提供“事件”机制,将服务与服务交互的耦合度降到最低。

其次,服务线上运行的动态伸缩问题。当下kubernetes环境下的弹性伸缩,需要由客户搜集监测数据,并自主手动来实现,但是我们更希望服务线上能够更加自动化和智能化。

最后,服务标准化问题。我们希望服务内部的模型是标准的、能够快速复制和快速构建的;服务通信是标准的:协议标准,格式标准;运行环境是标准的:快速部署,快速迁移。

Knative的出现恰好解决远程直接调用,服务线上自动管理以及一些列标准化问题。

下面我们来看一下三者的关联:


微信图片_20190603102740.png



Kubernetes和Istio相信大家比较熟悉了,这里不做过多介绍,有需要的同学可以关注下我们之前发布的相关文章,这里我们重点来看一下Knative。

Knative是谷歌开源的serverless架构方案,旨在提供一套简单易用的serverless方案,把serverless标准化。目前参与的公司主要是Google、Pivotal、IBM、Red Hat,于2018年7月份对外发布,目前处于快速发展阶段。


Knative组成


Build
构建系统:把用户定义的应用构建成容器镜像,面向kubernetes的标准化构建,区别于Dockerfile镜像构建,重点解决kubernetes环境的构建标准化问题。

Serving
服务系统:利用Istio的部分功能,来配置应用路由,升级以及弹性伸缩。Serving中包括容器生命周期管理,容器外围对象(service,ingres)生成(恰到好处的把服务实例与访问统一在一起),监控应用请求,自动弹性负载,并且利用Virtual service和destination配置服务访问规则。只有这样才能保证服务呈现一致性以及服务运行自动化管理。

Eventing
事件系统:用于自动完成事件的绑定与触发。事件系统与直接调用最大的区别在于响应式设计,它允许运行服务本身不需要屏蔽了调用方与被调用方的关系。从而在业务层面能够实现业务的快速聚合,或许为后续业务编排创新提供事件。

现在我们换一个角度,聚焦应用服务生命周期:
**· Knative 解决应用模板+面向统一环境的标准化构建场景;
· Kubernetes作为基础设施,解决应用编排和运行环境场景;
· 加粗文字Isito作为通信基础设施层,保证应用服务运行可检测、可配置、可追踪问题。**

这三者贯穿应用服务生命周期全过程,容器云恰恰也是管理应用服务的控制平台,这就能够很好地解释,为什么Kubernetes,Istio,Knative在未来会成为构建容器云的三驾马车。

GitLab CI/CD 在 Node.js 项目中的实践

尼古拉斯 发表了文章 • 0 个评论 • 178 次浏览 • 2019-06-03 09:58 • 来自相关话题

【编者的话】近期在按照业务划分项目时,我们组被分了好多的项目过来,大量的是基于 Node.js 的,也是我们组持续在使用的语言。 #现有流程中的一些问题 在维护多个项目的时候,会暴露出一些问题: 如何有效的使用 ...查看全部
【编者的话】近期在按照业务划分项目时,我们组被分了好多的项目过来,大量的是基于 Node.js 的,也是我们组持续在使用的语言。
#现有流程中的一些问题
在维护多个项目的时候,会暴露出一些问题:

  1. 如何有效的使用 测试用例
  2. 如何有效的使用 ESLint
  3. 部署上线还能再快一些吗

* 使用了 TypeScript 以后带来的额外成本

##测试用例
首先是测试用例,最初我们设计在了 git hooks 里边,在执行 git commit 之前会进行检查,在本地运行测试用例。 如果你想和更多 GitLab 技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态。

这会带来一个时间上的问题,如果是日常开发,这么操作还是没什么问题的,但如果是线上 bug 修复,执行测试用例的时间依据项目大小可能会持续几分钟。

而为了修复 bug,可能会采用 commit 的时候添加 -n 选项来跳过 hooks,在修复 bug 时这么做无可厚非,但是即使大家在日常开发中都采用commit -n 的方式来跳过繁琐的测试过程,这个也是没有办法管控的,毕竟是在本地做的这个校验,是否遵循这个规则,全靠大家自觉。

所以一段时间后发现,通过这种方式执行测试用例来规避一些风险的作用可能并不是很有效。
##ESLint
然后就是 ESLint,我们团队基于airbnb的 ESLint 规则自定义了一套更符合团队习惯的规则,我们会在编辑器中引入插件用来帮助高亮一些错误,以及进行一些自动格式化的操作。

同时我们也在 git hooks 中添加了对应的处理,也是在 git commit 的时候进行检查,如果不符合规范则不允许提交。

不过这个与测试用例是相同的问题:

  1. 编辑器是否安装 ESLint 插件无从得知,即使安装插件、是否人肉忽略错误提示也无从得知。
  2. git hooks 可以被绕过

##部署上线的方式
之前团队的部署上线是使用shipit周边套件进行部署的。

部署环境强依赖本地,因为需要在本地建立仓库的临时目录,并经过多次ssh XXX "command"的方式完成部署 + 上线的操作。

shipit提供了一个有效的回滚方案,就是在部署后的路径添加多个历史部署版本的记录,回滚时将当前运行的项目目录指向之前的某个版本即可。(不过有一点儿坑的是,很难去选择我要回滚到那个节点,以及保存历史记录需要占用额外的磁盘空间)

不过正因为如此,shipit在部署多台服务器时会遇到一些令人不太舒服的地方。

如果是多台新增的服务器,那么可以通过在shipit配置文件中传入多个目标服务器地址来进行批量部署。

但是假设某天需要上线一些小流量(比如四台机器中的一台),因为前边提到的shipit回滚策略,这会导致单台机器与其他三台机器的历史版本时间戳不一致(因为这几台机器不是同一时间上线的)

了这个时间戳就另外提一嘴,这个时间戳的生成是基于执行上线操作的那台机器的本地时间,之前有遇到过同事在本地测试代码,将时间调整为了几天前的时间,后时间没有改回正确的时间时进行了一次部署操作,代码出现问题后却发现回滚失败了,原因是该同事部署的版本时间戳太小,shipit 找不到之前的版本(shipit 可以设置保留历史版本的数量,当时最早的一次时间戳也是大于本次出问题的时间戳的)



也就是说,哪怕有一次进行过小流量上线,那么以后就用不了批量上线的功能了 (没有去仔细研究shipit官方文档,不知道会不会有类似--force之类的忽略历史版本的操作)

基于上述的情况,我们的部署上线耗时变为了: (__机器数量__)X(__基于本地网速的仓库克隆、多次 ssh 操作的耗时总和__)。 P.S. 为了保证仓库的有效性,每次执行 shipit 部署,它都会删除之前的副本,重新克隆

尤其是服务端项目,有时紧急的 bug 修复可能是在非工作时间,这意味着可能当时你所处的网络环境并不是很稳定。

我曾经晚上接到过同事的微信,让我帮他上线项目,他家的 Wi-Fi 是某博士的,下载项目依赖的时候出了些问题。

还有过使用移动设备开热点的方式进行上线操作,有一次非前后分离的项目上线后,直接就收到了联通的短信:「您本月流量已超出XXX」(当时还在用合约套餐,一月就800M流量)。
##TypeScript
在去年下半年开始,我们团队就一直在推动 TypeScript 的应用,因为在大型项目中,拥有明确类型的 TypeScript 显然维护性会更高一些。

但是大家都知道的, TypeScript 最终需要编译转换为 JavaScript(也有 tsc 那种的不生成 JS 文件,直接运行,不过这个更多的是在本地开发时使用,线上代码的运行我们还是希望变量越少越好)。

所以之前的上线流程还需要额外的增加一步,编译 TS。

而且因为shipit是在本地克隆的仓库并完成部署的,所以这就意味着我们必须要把生成后的 JS 文件也放入到仓库中,最直观的,从仓库的概览上看着就很丑(50% TS、50% JS),同时这进一步增加了上线的成本。

总结来说,现有的部署上线流程过于依赖本地环境,因为每个人的环境不同,这相当于给部署流程增加了很多不可控因素。
#如何解决这些问题
上边我们所遇到的一些问题,其实可以分为两块:

  1. 有效的约束代码质量
  2. 快速的部署上线

所以我们就开始寻找解决方案,因为我们的源码是使用自建的 GitLab 仓库来进行管理的,首先就找到了 GitLab CI/CD。

在研究了一番文档以后发现,它能够很好的解决我们现在遇到的这些问题。

要使用 GitLab CI/CD 是非常简单的,只需要额外的使用一台服务器安装 gitlab-runner,并将要使用 CI/CD 的项目注册到该服务上就可以了。

GitLab 官方文档中有非常详细的安装注册流程:

install | runner
register | runner
group register | repo 注册 Group 项目时的一些操作

上边的注册选择的是注册 group ,也就是整个 GitLab 某个分组下所有的项目。

主要目的是因为我们这边项目数量太多,单个注册太过繁琐(还要登录到 runner 服务器去执行命令才能够注册)
##安装时需要注意的地方
官网的流程已经很详细了,不过还是有一些地方可以做一些小提示,避免踩坑。
sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner

这是 Linux 版本的安装命令,安装需要 root (管理员) 权限,后边跟的两个参数:

* --user 是 CI/CD 执行 job (后续所有的流程都是基于 job 的)时所使用的用户名
* --working-directory 是 CI/CD 执行时的根目录路径 个人的踩坑经验是将目录设置为一个空间大的磁盘上,因为 CI/CD 会生成大量的文件,尤其是如果使用 CI/CD 进行编译 TS 文件并且将其生成后的 JS 文件缓存;这样的操作会导致 innode 不足产生一些问题

--user 的意思就是 CI/CD 执行使用该用户进行执行,所以如果要编写脚本之类的,建议在该用户登录的状态下编写,避免出现无权限执行 sudo su gitlab-runner



##注册时需要注意的地方
在按照官网的流程执行时,我们的 tag 是留空的,暂时没有找到什么用途。

以及 executor 这个比较重要了,因为我们是从手动部署上线还是往这边靠拢的,所以稳妥的方式是一步步来,也就是说我们选择的是 shell ,最常规的一种执行方式,对项目的影响也是比较小的(官网示例给的是 Docker)。
##.gitlab-ci.yml 配置文件
上边的环境已经全部装好了,接下来就是需要让 CI/CD 真正的跑起来
runner 以哪种方式运行,就靠这个配置文件来描述了,按照约定需要将文件放置到 repo 仓库的根路径下。

当该文件存在于仓库中,执行 git push 命令后就会自动按照配置文件中所描述的动作进行执行了。

* quick start
* configuration

上边的两个链接里边信息非常完整,包含各种可以配置的选项。

一般来讲,配置文件的结构是这样的:
stages:
- stage1
- stage2
- stage3

job 1:
stage: stage1
script: echo job1

job 2:
stage: stage2
script: echo job2

job 3:
stage: stage2
script:
- echo job3-1
- echo job3-2

job 4:
stage: stage3
script: echo job4

stages 用来声明有效的可被执行的 stage,按照声明的顺序执行。

下边的那些 job XXX 名字不重要,这个名字是在 GitLab CI/CD Pipeline 界面上展示时使用的,重要的是那个 stage 属性,他用来指定当前的这一块 job 隶属于哪个 stage。

script 则是具体执行的脚本内容,如果要执行多行命令,就像job 3那种写法就好了。

如果我们将上述的 stage、job 之类的换成我们项目中的一些操作install_dependencies、test、eslint之类的,然后将script字段中的值换成类似npx eslint之类的,当你把这个文件推送到远端服务器后,你的项目就已经开始自动运行这些脚本了。

并且可以在Pipelines界面看到每一步执行的状态。

P.S. 默认情况下,上一个 stage 没有执行完时不会执行下一个 stage 的,不过也可以通过额外的配置来修改:

* allow failure
* when

设置仅在特定的情况下触发 CI/CD

上边的配置文件存在一个问题,因为在配置文件中并没有指定哪些分支的提交会触发 CI/CD 流程,所以默认的所有分支上的提交都会触发,这必然不是我们想要的结果。

CI/CD 的执行会占用系统的资源,如果因为一些开发分支的执行影响到了主干分支的执行,这是一件得不偿失的事情。

所以我们需要限定哪些分支才会触发这些流程,也就是要用到了配置中的 only 属性。

使用only可以用来设置哪些情况才会触发 CI/CD,一般我们这边常用的就是用来指定分支,这个是要写在具体的 job 上的,也就是大致是这样的操作:

具体的配置文档
job 1:
stage: stage1
script: echo job1
only:
- master
- dev

单个的配置是可以这样写的,不过如果 job 的数量变多,这么写就意味着我们需要在配置文件中大量的重复这几行代码,也不是一个很好看的事情。

所以这里可能会用到一个yaml的语法:

这是一步可选的操作,只是想在配置文件中减少一些重复代码的出现



.access_branch_template: &access_branch
only:
- master
- dev

job 1:
<<: *access_branch
stage: stage1
script: echo job1

job 2:
<<: *access_branch
stage: stage2
script: echo job2

一个类似模版继承的操作,官方文档中也没有提到,这个只是一个减少冗余代码的方式,可有可无。
##缓存必要的文件
因为默认情况下,CI/CD在执行每一步(job)时都会清理一下当前的工作目录,保证工作目录是干净的、不包含一些之前任务留下的数据、文件。

不过这在我们的 Node.js 项目中就会带来一个问题。

因为我们的 ESLint、单元测试 都是基于 node_modules 下边的各种依赖来执行的。

而目前的情况就相当于我们每一步都需要执行npm install,这显然是一个不必要的浪费。

所以就提到了另一个配置文件中的选项:cache

用来指定某些文件、文件夹是需要被缓存的,而不能清除:
cache:
key: ${CI_BUILD_REF_NAME}
paths:
- node_modules/

大致是这样的一个操作,CI_BUILD_REF_NAME是一个 CI/CD 提供的环境变量,该变量的内容为执行 CI/CD 时所使用的分支名,通过这种方式让两个分支之间的缓存互不影响。
##部署项目
如果基于上边的一些配置,我们将 单元测试、ESLint 对应的脚本放进去,他就已经能够完成我们想要的结果了,如果某一步执行出错,那么任务就会停在那里不会继续向后执行。

不过目前来看,后边已经没有多余的任务供我们执行了,所以是时候将 部署 这一步操作接过来了。

部署的话,我们目前选择的是通过 rsync 来进行同步多台服务器上的数据,一个比较简单高效的部署方式。

P.S. 部署需要额外的做一件事情,就是建立从gitlab runner所在机器gitlab-runner用户到目标部署服务器对应用户下的机器信任关系。

有 N 多种方法可以实现,最简单的就是在runner机器上执行 ssh-copy-id 将公钥写入到目标机器。

或者可以像我一样,提前将 runner 机器的公钥拿出来,需要与机器建立信任关系时就将这个字符串写入到目标机器的配置文件中。

类似这样的操作:ssh 10.0.0.1 "echo \"XXX\" >> ~/.ssh/authorized_keys"
大致的配置如下:
variables:
DEPLOY_TO: /home/XXX/repo # 要部署的目标服务器项目路径
deploy:
stage: deploy
script:
- rsync -e "ssh -o StrictHostKeyChecking=no" -arc --exclude-from="./exclude.list" --delete . 10.0.0.1:$DEPLOY_TO
- ssh 10.0.0.1 "cd $DEPLOY_TO; npm i --only=production"
- ssh 10.0.0.1 "pm2 start $DEPLOY_TO/pm2/$CI_ENVIRONMENT_NAME.json;"

同时用到的还有variables,用来提出一些变量,在下边使用。

`ssh 10.0.0.1 "pm2 start $DEPLOY_TO/pm2/$CI_ENVIRONMENT_NAME.json;"`,这行脚本的用途就是重启服务了,我们使用pm2来管理进程,默认的约定项目路径下的pm2文件夹存放着个个环境启动时所需的参数。

当然了,目前我们在用的没有这么简单,下边会统一提到。

并且在部署的这一步,我们会有一些额外的处理。

这是比较重要的一点,因为我们可能会更想要对上线的时机有主动权,所以 deploy 的任务并不是自动执行的,我们会将其修改为手动操作还会触发,这用到了另一个配置参数:
deploy:
stage: deploy
script: XXX
when: manual # 设置该任务只能通过手动触发的方式运行

当然了,如果不需要,这个移除就好了,比如说我们在测试环境就没有配置这个选项,仅在线上环境使用了这样的操作。
##更方便的管理 CI/CD 流程
如果按照上述的配置文件进行编写,实际上已经有了一个可用的、包含完整流程的 CI/CD 操作了。

不过它的维护性并不是很高,尤其是如果 CI/CD 被应用在多个项目中,想做出某项改动则意味着所有的项目都需要重新修改配置文件并上传到仓库中才能生效。

所以我们选择了一个更灵活的方式,最终我们的 CI/CD 配置文件是大致这样子的(省略了部分不相干的配置):
variables:
SCRIPTS_STORAGE: /home/gitlab-runner/runner-scripts
DEPLOY_TO: /home/XXX/repo # 要部署的目标服务器项目路径

stages:
- install
- test
- build
- deploy_development
- deploy_production

install_dependencies:
stage: install
script: bash $SCRIPTS_STORAGE/install.sh

unit_test:
stage: test
script: bash $SCRIPTS_STORAGE/test.sh

eslint:
stage: test
script: bash $SCRIPTS_STORAGE/eslint.sh

# 编译 TS 文件
build:
stage: build
script: bash $SCRIPTS_STORAGE/build.sh

deploy_development:
stage: deploy_development
script: bash $SCRIPTS_STORAGE/deploy.sh 10.0.0.1
only: dev # 单独指定生效分支

deploy_production:
stage: deploy_production
script: bash $SCRIPTS_STORAGE/deploy.sh 10.0.0.2
only: master # 单独指定生效分支

我们将每一步 CI/CD 所需要执行的脚本都放到了 runner 那台服务器上,在配置文件中只是执行了那个脚本文件。

这样当我们有什么策略上的调整,比如说 ESLint 规则的变更、部署方式之类的。

这些都完全与项目之间进行解耦,后续的操作基本都不会让正在使用 CI/CD 的项目重新修改才能够支持(部分需要新增环境变量的导入之类的确实需要项目的支持)。
##接入钉钉通知
实际上,当 CI/CD 执行成功或者失败,我们可以在 Pipeline 页面中看到,也可以设置一些邮件通知,但这些都不是时效性很强的。

鉴于我们目前在使用钉钉进行工作沟通,所以就研究了一波钉钉机器人。

发现有支持 GitLab 机器人,不过功能并不适用,只能处理一些 issues 之类的, CI/CD 的一些通知是缺失的,所以只好自己基于钉钉的消息模版实现一下了。

因为上边我们已经将各个步骤的操作封装了起来,所以这个修改对同事们是无感知的,我们只需要修改对应的脚本文件,添加钉钉的相关操作即可完成,封装了一个简单的函数:
function sendDingText() {
local text="$1"

curl -X POST "$DINGTALK_HOOKS_URL" \
-H 'Content-Type: application/json' \
-d '{
"msgtype": "text",
"text": {
"content": "'"$text"'"
}
}'
}

# 具体发送时传入的参数
sendDingText "proj: $CI_PROJECT_NAME[$CI_JOB_NAME]\nenv: $CI_ENVIRONMENT_NAME\ndeploy success\n$CI_PIPELINE_URL\ncreated by: $GITLAB_USER_NAME\nmessage: $CI_COMMIT_MESSAGE"

# 某些 case 失败的情况下 是否需要更多的信息就看自己自定义咯
sendDingText "error: $CI_PROJECT_NAME[$CI_JOB_NAME]\nenv: $CI_ENVIRONMENT_NAME"

上述用到的环境变量,除了DINGTALK_HOOKS_URL是我们自定义的机器人通知地址以外,其他的变量都是有 GitLab runenr所提供的。

各种变量可以从这里找到:predefined variables
##回滚处理
聊完了正常的流程,那么也该提一下出问题时候的操作了。

人非圣贤孰能无过,很有可能某次上线一些没有考虑到的地方就会导致服务出现异常,这时候首要任务就是让用户还可以照常访问,所以我们会选择回滚到上一个有效的版本去。

在项目中的 Pipeline 页面 或者 Enviroment 页面(这个需要在配置文件中某些 job 中手动添加这个属性,一般会写在 deploy 的那一步去),可以在页面上选择想要回滚的节点,然后重新执行 CI/CD 任务,即可完成回滚。

不过这在 TypeScript 项目中会有一些问题,因为我们回滚一般来讲是重新执行上一个版本 CI/CD 中的 deploy 任务,在 TS 项目中,我们在 runner 中缓存了 TS 转换 JS 之后的 dist 文件夹,并且部署的时候也是直接将该文件夹推送到服务器的(TS项目的源码就没有再往服务器上推过了)。

而如果我们直接点击 retry 就会带来一个问题,因为我们的 dist 文件夹是缓存的,而 deploy 并不会管这种事儿,他只会把对应的要推送的文件发送到服务器上,并重启服务。

而实际上 dist 还是最后一次(也就是出错的那次)编译出来的 JS 文件,所以解决这个问题有两种方法:

  1. 在 deploy 之前执行一下 build
  2. 在 deploy 的时候进行判断

第一个方案肯定是不可行的,因为严重依赖于操作上线的人是否知道有这个流程。

所以我们主要是通过第二种方案来解决这个问题。

我们需要让脚本在执行的时候知道,dist 文件夹里边的内容是不是自己想要的。

所以就需要有一个 __标识__,而做这个标识最简单有效唾手可得的就是,git commit id。

每一个 commit 都会有一个唯一的标识符号,而且我们的 CI/CD 执行也是依靠于新代码的提交(也就意味着一定有 commit)。

所以我们在 build 环节将当前的commit id也缓存了下来:
git rev-parse --short HEAD > git_version

同时在 deploy 脚本中添加额外的判断逻辑:
currentVersion=`git rev-parse --short HEAD`
tagVersion=`touch git_version; cat git_version`

if [ "$currentVersion" = "$tagVersion" ]
then
echo "git version match"
else
echo "git version not match, rebuild dist"
bash ~/runner-scripts/build.sh # 额外的执行 build 脚本
fi

这样一来,就避免了回滚时还是部署了错误代码的风险。

关于为什么不将 build 这一步操作与 deploy 合并的原因是这样的:

因为我们会有很多台机器,同时 job 会写很多个,类似 deploy_1、deploy_2、deploy_all,如果我们将 build 的这一步放到 deploy 中。

那就意味着我们每次 deploy,即使是一次部署,但因为我们选择一台台机器单独操作,它也会重新生成多次,这也会带来额外的时间成本。
##hot fix 的处理
在 CI/CD 运行了一段时间后,我们发现偶尔解决线上 bug 还是会比较慢,因为我们提交代码后要等待完整的 CI/CD 流程走完。

所以在研究后我们决定,针对某些特定情况hot fix,我们需要跳过ESLint、单元测试这些流程,快速的修复代码并完成上线。

CI/CD 提供了针对某些 Tag 可以进行不同的操作,不过我并不想这么搞了,原因有两点:

  1. 这需要修改配置文件(所有项目)
  2. 这需要开发人员熟悉对应的规则(打 Tag)

所以我们采用了另一种取巧的方式来实现,因为我们的分支都是只接收Merge Request那种方式上线的,所以他们的commit title实际上是固定的:Merge branch 'XXX'。

同时 CI/CD 会有环境变量告诉我们当前执行 CI/CD 的 commit message。
我们通过匹配这个字符串来检查是否符合某种规则来决定是否跳过这些job:
function checkHotFix() {
local count=`echo $CI_COMMIT_TITLE | grep -E "^Merge branch '(hot)?fix/\w+" | wc -l`

if [ $count -eq 0 ]
then
return 0
else
return 1
fi
}

# 使用方法

checkHotFix

if [ $? -eq 0 ]
then
echo "start eslint"
npx eslint --ext .js,.ts .
else
# 跳过该步骤
echo "match hotfix, ignore eslint"
fi

这样能够保证如果我们的分支名为 hotfix/XXX 或者 fix/XXX 在进行代码合并时, CI/CD 会跳过多余的代码检查,直接进行部署上线。 没有跳过安装依赖的那一步,因为 TS 编译还是需要这些工具的。
#小结
目前团队已经有超过一半的项目接入了 CI/CD 流程,为了方便同事接入(主要是编辑 .gitlab-ci.yml 文件),我们还提供了一个脚手架用于快速生成配置文件(包括自动建立机器之间的信任关系)。

相较之前,部署的速度明显的有提升,并且不再对本地网络有各种依赖,只要是能够将代码 push 到远程仓库中,后续的事情就和自己没有什么关系了,并且可以方便的进行小流量上线(部署单台验证有效性)。

以及在回滚方面则是更灵活了一些,可在多个版本之间快速切换,并且通过界面的方式,操作起来也更加直观。

最终可以说,如果没有 CI/CD,实际上开发模式也是可以忍受的,不过当使用了 CI/CD 以后,再去使用之前的部署方式,则会明显的感觉到不舒适。(没有对比,就没有伤害

工作中99%能用到的Git命令

JetLee 发表了文章 • 0 个评论 • 149 次浏览 • 2019-06-03 09:09 • 来自相关话题

##分支操作 git branch 创建分支git branch -b 创建并切换到新建的分支上git checkout 切换分支git branch 查看分支列表git branch -v 查看所有分支的最后一次操作git bran ...查看全部
##分支操作

  1. git branch 创建分支
  2. git branch -b 创建并切换到新建的分支上
  3. git checkout 切换分支
  4. git branch 查看分支列表
  5. git branch -v 查看所有分支的最后一次操作
  6. git branch -vv 查看当前分支
  7. git brabch -b 分支名 origin/分支名 创建远程分支到本地
  8. git branch --merged 查看别的分支和当前分支合并过的分支
  9. git branch --no-merged 查看未与当前分支合并的分支
  10. git branch -d 分支名 删除本地分支
  11. git branch -D 分支名 强行删除分支
  12. git branch origin :分支名 删除远处仓库分支
  13. git merge 分支名 合并分支到当前分支上

##暂存操作

  1. git stash 暂存当前修改
  2. git stash apply 恢复最近的一次暂存
  3. git stash pop 恢复暂存并删除暂存记录
  4. git stash list 查看暂存列表
  5. git stash drop 暂存名(例:stash@{0})移除某次暂存
  6. git stash clear 清除暂存

##回退操作

  1. git reset --hard HEAD^ 回退到上一个版本
  2. git reset --hard ahdhs1(commit_id) 回退到某个版本
  3. git checkout -- file撤销修改的文件(如果文件加入到了暂存区,则回退到暂存区的,如果文件加入到了版本库,则还原至加入版本库之后的状态)
  4. git reset HEAD file 撤回暂存区的文件修改到工作区

##标签操作

  1. git tag 标签名 添加标签(默认对当前版本)
  2. git tag 标签名 commit_id 对某一提交记录打标签
  3. git tag -a 标签名 -m '描述' 创建新标签并增加备注
  4. git tag 列出所有标签列表
  5. git show 标签名 查看标签信息
  6. git tag -d 标签名 删除本地标签
  7. git push origin 标签名 推送标签到远程仓库
  8. git push origin --tags 推送所有标签到远程仓库
  9. git push origin :refs/tags/标签名 从远程仓库中删除标签

##常规操作

  1. git push origin test 推送本地分支到远程仓库
  2. git rm -r --cached 文件/文件夹名字 取消文件被版本控制
  3. git reflog 获取执行过的命令
  4. git log --graph 查看分支合并图
  5. git merge --no-ff -m '合并描述' 分支名 不使用Fast forward方式合并,采用这种方式合并可以看到合并记录
  6. git check-ignore -v 文件名 查看忽略规则
  7. git add -f 文件名 强制将文件提交

##Git创建项目仓库

  1. git init 初始化
  2. git remote add origin url 关联远程仓库
  3. git pull
  4. git fetch 获取远程仓库中所有的分支到本地

##忽略已加入到版本库中的文件

  1. git update-index --assume-unchanged file 忽略单个文件
  2. git rm -r --cached 文件/文件夹名字 (. 忽略全部文件)

##取消忽略文件

  1. git update-index --no-assume-unchanged file

##拉取、上传免密码

  1. git config --global credential.helper stor

原文链接:https://mp.weixin.qq.com/s/r7HVSeoBw48F9adI6v9aiw