总篇116篇 2021年第7篇
1. ASF 是什么
之家云微服务平台ASF(AutoHome Service Framework),提供服务注册和发现、服务治理、可观测、动态配置等能力,致力于帮助之家用户一站式构建微服务体系以及服务上云的能力。
ASF1.0 主要针对 Java 应用,支持使用 Dubbo 和 Spring Cloud 进行接入。随着业务的多元化发展,越来越多的用户选择使用 Go 语言和 gRPC 构建微服务,但是 gRPC 的服务注册/发现、负载均衡、健康检查等功能需要用户进行开发和维护,另外 gRPC 服务治理能力也比较弱。
在ASF 2.0 中我们增加了对 Dubbo-go 的支持,将上述复杂的功能沉淀到平台侧,为 Java 和 Go 提供了统一的、标准的服务治理能力,极大的降低了使用 Go 语言构建微服务的成本。本篇文章主要介绍之家云 ASF 在使用 Dubbo-go 构建微服务并进行大规模服务治理时的探索和实践。
如下图所示,深蓝色虚线框内为 ASF 提供的主要功能,绿色虚线框是即将要交付的功能。
下载 gRPC pb插件,生成 pb.go 文件。
编写服务端代码
const (
port = ":50051"
)
type SpecialService struct {
pb.UnimplementedSpecialServer
}
func (s *SpecialService) Call(ctx context.Context, req *pb.SpecialRequest) (reply *pb.SpecialReply, err error) {
fmt.Printf("req: %v\n", req)
return &pb.SpecialReply{Message: "Hello ASF Go : " + port}, nil
}
func main() {
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// 注册到grpc
s := grpc.NewServer()
protobuf.RegisterGreeterServer(s, &SpecialService{})
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
go get -u github.com/apache/dubbo-go/protocol/grpc/protoc-gen-dubbo@v1.5.7
// 注册到dubbo-go
func init() {
config.SetProviderService(NewSpecialService())
}
------ 迁移部分---
type SpecialService struct {
*pb.SpecialProviderBase
}
func NewSpecialService() *SpecialService {
return &SpecialService{
SpecialProviderBase: &pb.SpecialProviderBase{},
}
}
------ 迁移部分---
// 业务代码
func (s *SpecialService) Call(ctx context.Context, req *pb.SpecialRequest) (reply *pb.SpecialReply, err error) {
fmt.Printf("req: %v\n", req)
return &pb.SpecialReply{Message: "Hello ASF Go : " + port}, nil
}
services:
"specialProvider": # 服务的唯一标识,pb中自动生成
version: 1
group: "demo"
protocol: "grpc"
interface: "asf.smc.cloud.base.special"
cluster: failover # 集群容错策略
loadbalance: random # 负载均衡策略
CompositeConfig
,如下:type CompositeConfig struct {
ProviderConfig *ProviderConfig `yaml:"provider" json:"provider,omitempty" property:"provider"`
ConsumerConfig *ConsumerConfig `yaml:"consumer" json:"consumer,omitempty" property:"consumer"`
Services map[string]*ServiceConfig `yaml:"services" json:"services,omitempty" property:"services"`
References map[string]*ReferenceConfig `yaml:"references" json:"references,omitempty" property:"references"`
}
CompositeConfig
聚合了 provider 和 consumer 的配置,并且在compositeConfig
抽象了 consumer 和 provider语义。consumer:
check: false # 关闭启动时检查
provider:
filter: "echo" # provider 启动的过滤器
export CONF_PROVIDER_FILE_PATH="../profiles/dev/server.yml"
export CONF_CONSUMER_FILE_PATH="../profiles/dev/server.yml"
export APP_LOG_CONF_FILE="../profiles/dev/log.yml"
## 生产者
go run . -proConf ../profiles/dev/server.yml -logConf ../profiles/dev/log.yml
## 消费者
go run . -conConf ../profiles/dev/client.yml -logConf ../profiles/dev/log.yml
// 服务提供方配置
PostProcessServiceConfig(*common.URL)
// 服务消费方配置
PostProcessReferenceConfig(*common.URL)
config.LoadWithConfig(&common.AsfConfig{
CompositeConf: "./asf-grpc/server/server.yml",
Level: "debug",
})
providerExportedHook := config.NewProviderExportedHook(func(event observer.Event) {
logger.Infof("provider exported %s", event.GetTimestamp().Format("3006-01-02 15:04:05"))
})
consumerReferencedHook := config.NewConsumerReferencedHook(func(event observer.Event) {
logger.Infof("consumer referenced %s", event.GetTimestamp().Format("3006-01-02 15:04:05"))
})
bootstarp := config.NewAsfBootstarp().
AddProviderExportedHook(providerExportedHook).
AddConsumerReferencedHook(consumerReferencedHook).
ConfigPath("./asf-grpc/server/server.yml").
LoadConfig(). // 加载配置
// DelayExport(true). // 加载配置后 配置延迟暴露
Start()
dubbo.protocol.name=dubbo
dubbo.protocol.port=12345
dubbo.config.multiple=true
dubbo.registry.id=mbp
dubbo.registry.address=nacos://192.168.1.31:8848
dubbo.registry.zone=mbp
dubbo.registry.use-as-config-center=false
dubbo.registry.zone
属性,在发布应用的时候需要按照 AZ 加载不同的配置文件,步骤相当的繁琐。上游集群 AZ 节点数量分布不均匀。假设 provider A 部署在 AZ1 和 AZ2 中,但是AZ1 中只有1个可用实例,而 AZ2 中有5个。在这种场景下会导致 AZ1 中的 consumer 的所有流量都路由到 AZ1 中的那一个实例上。
ASF 在负载均衡逻辑中增加了恐慌阀值判断,当 AZ / Region < 30% 的时候直接在 Region 所有可用实例进行路由,这样就可以保证不会把流量集中到过少的节点,导致服务处于“雪崩”的状态。
如下图所示,始发集群 AZ 节点数量大于上游集群 AZ 节点数量。AZ1 中的 consumer 节点数量远大于 AZ1 的provider 数量,此时如果将流量全部路由到 AZ1 中的 provider 会导致明显的负载均衡不均匀,因此我们需要将多余的流量路由到 AZ2。
支持使用 push 模式进行数据上报,因为 pull 模式需要 prometheus 支持服务发现,而一些主流的注册中心,比如naocs 还不支持 prometheus。额外暴露一个 port,端口的安全策略也需要保证。若同一个 IP 部署了多个应用,则会导致 port 难以获取,实现起来比较复杂。
去掉了 Summary , 在Prometheus中,Histogram 会预先划分若干个bucket,Histogram 不会保存数据采样点值,每个bucket只记录落在本区间样本数的counter,即histogram存储的是区间的样本数统计值,在服务端(prometheus)我们可以通过 histogram_quantile()[3] 函数来计算分位数,因此比较适合高并发的数据收集。而 Summary 则在客户端 直接存储了 quantile 数据,不适合高并发的场景。
使用 Histogram 重要的是 bucket 的划分,bucket 太多了或者太少了都不行,而 dubbo-go 提供的划分比较简单。ASF 参考了 micrometer PercentileHistogramBuckets[4]的划分算法,这个算法是Netfix根据经验值而划分的,预先生成了 1ms-60s 的72个桶。
优化了 dubbo-go 的 metrics,根据谷歌的黄金法则 ASF 采集的指标可以计算,服务的RT、QPS、请求的成功/失败比率、饱和度(dubbo-go协程数量,暂时没有采集)、TP99(反映尾部延迟[5])。
上云后 K8S 管理的 Node 的性能不均衡,新机器的性能好老机器的性能差,尤其是在大促的时候会增加很多的新机器。而 K8S 在调度Pod的时候是随机的,我们的期望是性能好的、新的机器处理更多的流量,而性能差的、老的机器处理少量的流量,所以简单的轮询算法(Round Robin)和随机(Random)算法并不能满足我们上云之后的需求。
针对第一个问题,我们可以使用加权轮随机算法(Weight Round Robin),加权最小活跃数算法(LeastActive)。然而 dubbo 的负载均衡算法不支持动态修改权重,需要人工运维,并且最小活跃数算法存在“羊群效应”,极端情况下性能好的机器将处理所有流量,性能差的机器没有流量。
故障自愈能力,我们希望负载均衡算法能够自动摘除故障的节点,并具备故障自愈能力。
羊群效应
,并通过指数加权移动平均算法(EWMA)
统计节点的实时状态,从而做出最优选择。诸如 Linkerd[6]、Rsocket[7] 等知名项目,都采用的是P2C负载均衡算法。vt
代表第t次请求的指数加权值,vt-1
代表上次请求的指数加权平均值,θt
代表第t次请求的值。算数平均算法对网络耗时不敏感, 而 EWMA
可以在网络波动时适当降低β的值
,使其快速感知到波动的存在,当网络波动结束后,适当提升β的值
,这样就可以在网络稳定的情况下较好的反映一个区段内的均值情况。
对于 request-response
交互模型来说,使用算数平均算法计算节点权重,因为信息滞后会产生“羊群效应”。
EWMA
算法不需要保存过去的所有值,计算量小,并且节省内存。
在实际的测试中,我们发现在request-response模型中,EWMA 算法对频繁的网络波动场景下并没有达到预期的效果,并且在另外的一些框架中P2C算法也存在预热问题。我们将会联合 dubbo-go 社区共同完善 P2C 算法的实现,并在柔性服务上做出更多的探索。
readinessProbe
再去暴露服务。否则再上线时可能会出现大量的调用错误。延迟暴露
和Qos
来解决上述问题,但是延迟暴露依赖于一个经验值,而QOS则依赖于发布系统,似乎都不是很完美。未来在 ASF 中将可以直接通过Bootstarp做到无损上线,实例如下:providerExportedHook := config.NewProviderExportedHook(func(event observer.Event) {
logger.Infof("provider exported %s", event.GetTimestamp().Format("3006-01-02 15:04:05"))
})
consumerReferencedHook := config.NewConsumerReferencedHook(func(event observer.Event) {
logger.Infof("consumer referenced %s", event.GetTimestamp().Format("3006-01-02 15:04:05"))
})
bootstarp := config.NewAsfBootstarp().
AddProviderExportedHook(providerExportedHook).
AddConsumerReferencedHook(consumerReferencedHook).
ConfigPath("./asf-grpc/server/server.yml").
LoadConfig(). // 加载配置
DelayExport(true). // 加载配置后 配置延迟暴露
Start()
time.Sleep(time.Second * 5) // wanm-up
bootstarp.StartProvider() // 暴露服务
作者简介
参考资料
跨地域场景下,如何解决分布式系统的一致性?: https://mp.weixin.qq.com/s/05-ko4KyeHOTkrAbl8dQwQ
[2]dubbo 多注册中心负载均衡: https://dubbo.apache.org/zh/blog/3019/06/32/%E4%BD%BF%E7%94%A8-dubbo-%E8%BF%9E%E6%8E%A5%E5%BC%82%E6%9E%84%E5%BE%AE%E6%9C%8D%E5%8A%A1%E4%BD%93%E7%B3%BB/#dubbo-%E4%BD%93%E7%B3%BB%E5%86%85%E7%9A%84%E5%A4%9A%E5%8D%8F%E8%AE%AE%E5%A4%9A%E6%B3%A8%E5%86%8C%E4%B8%AD%E5%BF%83%E6%9C%BA%E5%88%B6
[3]histogram_quantile: https://prometheus.io/docs/prometheus/latest/querying/functions/#histogram_quantile
[4]PercentileHistogramBuckets: https://github.com/micrometer-metrics/micrometer/blob/main/micrometer-core/src/main/java/io/micrometer/core/instrument/distribution/PercentileHistogramBuckets.java
[5]who-moved-my-99th-percentile-latency: https://engineering.linkedin.com/performance/who-moved-my-99th-percentile-latency
[6]知名的服务网格项目: https://linkerd.io/3.10/features/load-balancing/
[7]Netfix 开源的基于响应式编程的应用层协议: https://rsocket.io/