为什么要采样追踪对接SkyWalking

为了提升Pulsar的可维护性,我们希望能深入pulsar和bookkeeper的底层,做性能剖析,识别中间延时高的环节,然后很方便地进行定位分析。

为什么Pulsar原生的普罗监控无法满足

  • 部门基础设施方面,所有环境都集成了ELK,大部分环境都没有普罗,且部门正在引入SkyWalking。
  • 普罗的监控无法和用户的一条消息对应,较难处理单个客户保障。

幸运的是,这两个问题都可以通过采样跟踪来解决。并且在合理的采样跟踪配置下,测试环境可以达成百分百采样追踪,对于我们定位测试环境问题非常方便。

我们就想到了使用经典的采样追踪模式来对数据进行采样,将数据输出到SkyWalking进行下一步的分析、告警。并且在没有对接SkyWalking的环境,通过日志输出来进行下一步的分析、告警

Sample Tracing Basic

image-20210424091405442

  • 通过traceId和spanId结合,识别一条链路。
  • 每个spanId中间计算耗时
  • 为了性能,不会每个spanId和traceId都收集分析,会进行采样(收集部分消息、收集时延较大的消息)

注:SkyWalking采用STAM来进行拓扑分析,并且引入了Segment等概念来表达进程内、进程外等含义,但大致原理相同。

SkyWalking Architecture

Image

Trace Internal In Pulsar

目前,我们将追踪的头部消息放在BrokerMetadata处,以下为追踪数据流向

message

image-20210506200048088

batch message

image-20210506200035405

Two ways output

Logging

可在ELK上展示,搜索时延大的消息。(注:如搜索4位数的消息)。

SkyWalking

可依赖SkyWalking进行分析,告警。

对比

Logging SkyWalking
采样方式 SkyWalking Way SkyWalking Way
输出格式 Logging SkyWalking protocol through kafka
追踪数据传播方式 protobuf、Broker Metadata protobuf、Broker Metadata

Beyond SkyWalking

在SkyWalking的采样算法之上,支持对时延大的消息再进行采样,输出到日志,进行分析、告警。

采样规则

  • 全局一秒最多xx条,单topic一秒最多xx条
  • 采样时延大于xx的消息

Follow-up

  • 将Pulsar和Bookkeeper的metric信息上报到SkyWalking,使得在SkyWalking分析、告警更多信息。
  • 使用agent方式实现,避免和SkyWalking协议过度耦合

参考

之前就在环境上ps -ef看到过xxxxxx的密码,一直没搞明白怎么回事,今天整理了一下,核心内容均来自于上述连接,作了一些额外的测试和查阅资料。

测试

运行Mysql实例

1
2
# 自己做的Mysql8的镜像
docker run ttbb/mysql:stand-alone

使用密码连接Mysql服务器

1
mysql -u hzj -p Mysql@123 -e "select 1"

ps -ef查看

1
2
3
4
5
6
7
8
9
10
[root@91bcbd15a82e mysql]# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 07:34 ? 00:00:00 /usr/local/bin/dumb-init bash -vx /opt/sh/mysql/hzj/scripts/start.sh
root 8 1 0 07:34 ? 00:00:00 bash -vx /opt/sh/mysql/hzj/scripts/start.sh
root 17 1 0 07:34 ? 00:00:00 mysqld --daemonize --user=root
root 62 8 0 07:34 ? 00:00:00 tail -f /dev/null
root 63 0 0 07:34 pts/0 00:00:00 bash
root 98 63 0 07:37 pts/0 00:00:00 mysql -h 127.0.0.1 -u hzj -px xxxxxxx
root 99 0 1 07:37 pts/1 00:00:00 bash
root 122 99 0 07:37 pts/1 00:00:00 ps -ef

Mysql隐藏密码原理

改写了args系统参数,demo如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//
// Created by 张俭 on 2021/4/26.
//
#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char *argv[]) {
int i = 0;
pid_t mypid = getpid();
if (argc == 1)
return 1;
printf("argc = %d and arguments are:\n", argc);
for (i; i < argc; i++) {
printf("%d = %s\n", i, argv[i]);
}
fflush(stdout);
sleep(30);
printf("Replacing first argument with x:es... Now open another terminal and run: ps p %d\n", (int)mypid);
memset(argv[1], 'x', strlen(argv[1]));
getc(stdin);
return 0;
}

编译并运行

1
2
3
4
5
6
7
gcc password_hide.c
[root@c77dc365cd1a sh]# ./a.out abcd
argc = 2 and arguments are:
0 = ./a.out
1 = abcd
Replacing first argument with x:es... Now open another terminal and run: ps p 55

观测结果,开始看的确有明文密码

1
2
3
4
5
6
[root@c77dc365cd1a sh]# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 07:49 pts/0 00:00:00 bash
root 32 0 0 07:51 pts/1 00:00:00 bash
root 64 1 0 07:56 pts/0 00:00:00 ./a.out abcd
root 66 32 0 07:56 pts/1 00:00:00 ps -ef

经过30秒后,已经被复写

1
2
3
[root@c77dc365cd1a sh]# ps p 55
PID TTY STAT TIME COMMAND
55 pts/0 S+ 0:00 ./a.out xxxx

Mysql源码地址

mysql-server/client/mysql.cc line 2054

1
2
3
4
5
6
7
8
9
if (argument) {
char *start = argument;
my_free(opt_password);
opt_password = my_strdup(PSI_NOT_INSTRUMENTED, argument, MYF(MY_FAE));
while (*argument) *argument++ = 'x'; // Destroy argument
if (*start) start[1] = 0;
tty_password = false;
} else
tty_password = true;

PS: 后面,我还在OSX上用go程序尝试修改参数,估摸go程序的args传入是值拷贝,修改完成之后args没有生效,看来这个黑科技只有c程序能使用呀。

需要了解的概念

  • VPC:用户的私有网段
  • peering:多个VPC之间打通的方式,可跨用户

前言

今天微信推送Pulsar社区有个Hackathon比赛, 开始想的idea就是,实现pulsar在华为云上提供服务。因为是社区的比赛,是以一个三方系统的方式在华为云上提供服务,而非是以华为云的名义提供服务。分析了下可行性和能达到的效果,对比了StreamNative的官网上提供的pulsar服务在阿里云托管的能力,能提供的能力差不多,最多只不过是实现了在华为云托管的能力,没有从0到1的突破。

现在,在公有云上买redis和kafka这类组件已经变得非常普遍,由公有云供应商提供的中间件往往能给你带来良好的体验,相比三方厂家在云上进行托管,我个人认为云厂商的优势主要在以下三点

网络打通容易

下文说一下不是公有云的供应商能以什么样的方式暴露自己的服务。云厂商可以把中间件的ip地址申请在你的vpc内,对任何应用程序来说,连接都是最方便的。无论是容器化部署、虚拟机部署、和其他vpc peering打通的场景,都可以通信。

低廉的成本

不考虑人力成本,云厂商自运营的价格要低于三方厂家。

监控系统对接

方便地和云厂商的告警、统计系统对接,接收告警通知和报表等。

其中网络打通和成本尤为重要,三方厂家好好做监控统计系统,也能给用户较为良好的体验。

三方厂家能提供什么样的Pulsar接入

统一接入

三方厂家自己作为公有云上一个用户,无论这个Region上有多少个租户,都用这一个用户提供服务,这也就意味着无法与每个用户进行私网通信。如果在华为云,利用华为云推出的VPCEP服务(此处应有链接),倒是可以给每个用户提供私网通信,不过这个是做了DNAT地址转换的,跟做了DNAT转换的中间件连接,是非常麻烦的。(懂的自然懂。如果有人想详细了解,可以留言,我可以写一个文章介绍里面的坑)

如果使用公网,又想避免扩容的时候动态申请EIP,动态申请EIP并不复杂,问题是EIP是有配额限制的,这才是关键。那么就需要一个统一的接入点,就需要部署pulsar proxy。到这一步,是每个用户申请一个EIP的,如果还想继续节省EIP,那么可以统一域名接入,后端通过SNI的方式转发,个别流量大的客户,单独把域名指向单独的集群。

pulsar-third-vendor1

Peering打通

Peering打通可以给用户不错的私网体验,需要用户预留一个网段,网段不需要太大,能容纳pulsar所在的vm就行。采用peering打通一般绝不会选择容器化部署,想要两个容器化的集群互通,对网设的要求很高,暂且忽略Service的存在,这要求用户的vpc网段和pod网段和三方厂商的vpc网段和pod网段都不重叠!而且peering打通,给用户私有,再搭建一个k8s集群,对成本影响比较大。主要有如下两个问题

自动化

和客户peering打通,需要较大的权限,如何自动化,最大程度的减少需要的权限。

客户网段和其他网段又做了peering

pulsar-third-vendor2

这个问题其实还好,就是路由规则配置麻烦

总结

Peering打通对用户来说已经比较方便了,相信做到自动化也没有太大的技术难度,只是时间和人力投入的问题。统一接入因为网络打通的原因,不好使用kop、mop这些高级特性,此外还有不小的公网带宽成本,羊毛出在羊身上,比较大量的用户也会倾向于Peering打通的模式吧。

前言

自17年入职华为之后,一直在使用配置中心,4年期间经历了自研配置中心到Apollo再到自研配置中心和Apollo并存的场景。总结了一下这几年的配置中心演进流程,想把我们在配置中心上的一些实践分享给大家,实现共同进步。Apollo是一款非常优秀的开源软件,是国人的骄傲。如果对Apollo存在理解错误,还望大家不吝赐教,谢谢。

使用到的配置分类

从场景分类

运维配置,即程序只读的配置

人工配置。通过人工在配置中心界面进行配置,而程序只进行读取,如数据库配置、邮箱服务器配置、网卡配置、子网地址配置等。这部分配置数据不要求代码动态写入。

业务配置,即程序可写的配置

我们是一个SaaS服务,每个用户在上面都有一些业务配置。如用户的证书配置、用户服务器的流控配置等,这些业务配置相对运维配置来说更加复杂,且可能会有唯一性限制,如按用户id唯一。这部分配置数据一般由用户操作触发,代码动态写入,并且通知到各个微服务实例。通常,我们希望这些配置能在界面展示,且支持人为修改。上述逻辑如果由各微服务自己实现,会存在大量重复代码,并且质量无法保证。我们希望由一个公共组件来统一实现这个能力。

从配置是否会有列表可分为单值配置或多值配置

单值配置

整个配置下只是多对key、value。value不是很复杂的格式,往往是整数或字符串。

image-20210330171658154

多值配置

多值配置更加复杂,往往是单值配置在不同的key下,有不同的值。比如下面的配置,用户一和用户二的线程池大小和队列不同

img

第一阶段 自研配置中心

在做云服务之前,我们的配置中心层级数较少。我们以软件的形式交付给客户,软件运行时分为管理面和业务面,配置中心管理着管理面和业务面的配置,最为复杂的场景是多套业务面,这个时候需要保证不同集群、不同微服务下的配置不冲突,配置层级为 集群、微服务、配置。

image-20210324204231586

此时的配置中心是完全自研的,不包含蓝绿、灰度配置这些功能,它独具特色的地方有以下两点:

单配置单表

  • 在存储模型上,每个配置对应一张数据表。
  • 对多值配置比较友好,尤其是复杂业务配置,可以支持各种主键约束。对单值配置,稍微重型了一些。
  • 配置的强Schema限制。这些限制包括类型、大小、长度、是否敏感等限制。这种限制既能为界面修改配置提供良好的体验(如:不同格式不同的输入框、敏感字段,前台输入明文,后台入库加密等),也能在通过接口写入配置时做充分的校验。

通过回调方式来确保配置的可靠

举个例子,添加一个配置的流程是这样的

image-20210324205828998

可能这里,有读者想要问了,这个流程能确保什么可靠呢。这个流程通过调用微服务接口来校验配置是否可靠,如IP地址是否合法、对端地址是否可达、配置数量是否超过规格等等,来保证配置基本可用。

总的来说,这个自研的配置中心在当时综合体验还是不错的。但是也有一些问题有待改进,比如单配置下配置项数量过多时,因为底层有部分接口单配置下所有数据都通过一个http请求来承载,会导致响应超时等问题。

第二阶段 Apollo

开始第二阶段实践的原因主要是,我们进行了组织切换,业务重心转向做云服务,同时团队进行DevOps转型。原先的老配置中心是由另一个团队维护的,组织切换完之后,如果还要使用,就要我们自己维护。所以我们需要在继续维护老配置中心和引入开源Apollo中间进行选择。除了上文中提到的运维配置和业务配置,这个时候我们的需求还有改变:

  • 配置的层级愈发丰富了
  • 要构建灰度发布微服务的能力

老配置中心一方面由于组织切换原因不提供维护了,另一方面不能支撑丰富的配置层级,也不具备灰度发布的能力。这个时候,Apollo的一些特性吸引了我们,这些特性正是老配置中心所缺乏的,例如(部分引用自Apollogithub主页)

  • 丰富的层级,从app_id到cluster,namespace,key-value的层级能满足我们region、集群、微服务的层级诉求
  • 支持配置的灰度发布,比如点了发布后,只对部分应用实例生效,等观察一段时间没问题后再推给所有应用实例。
  • 所有的配置发布都有版本概念,从而可以方便的支持配置的回滚。
  • 应用和配置的管理都有完善的权限管理机制,对配置的管理还分为了编辑和发布两个环节,从而减少人为的错误。
  • 所有的操作都有审计日志,可以方便的追踪问题。

因此我们选型引入了Apollo,我和我的主管,还有一个其他同事参与了这项工作。我们在Apollo开源代码的基础上做了比较大的改动,主要原因有以下几点

  • 节约成本,将注册中心、数据库替换成我们当前正在使用的组件,因为这两个依赖不是Apollo的核心依赖
  • 继承老配置中心强Schema的优点。
  • 保留回调确认配置的流程,提前拦截错误的配置,降低代码处理异常配置的复杂度
  • 通过spi或环境变量的方式兼容存量老局点使用老配置中心的场景

结合上述原因,我们最终是这么实践的

  • 数据库切换为postgre数据库、注册中心切换到servicecomb

  • 在namespace上实现了Schema,每个namespace都可以注册对应的Schema,Schema要求数据必须是json格式,且json内对应的value必须满足Schema定义的规范(如ip地址、小数、整数等)

    Schema举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[
{
"name":"name",
"type":"string"
},
{
"name":"age",
"type":"int",
"max":120
},
{
"name":"ip",
"type":"ipv4"
}
]

那么数据应该是这样的

1
2
3
4
5
{
"name":"hezhangjian",
"age":23,
"ip":"127.0.0.1"
}
  • 在添加或修改配置的时候,实现了回调功能,由回调业务服务确认配置能否添加或修改
  • 配置分层:云服务对应Apollo的app_id,把内部的环境对应到Apollo上的集群,然后将微服务名+配置名拼接成配置名称。

下图展示了业务概念和Apollo概念的对应关系,有些配置是单值配置,有些是多值配置,所以配置项这一层级是可选的。

配置分层示意图

在这段时间的实践中,我们也发现如下问题

并发问题

其中最致命的就是并发问题,首先Apollo所有配置都存在一张表中,其次由于Apollo设计之初主要考虑的是运维人员手动在界面上操作,代码无并发语义(或者说没给客户端并发语义),使得我们通过代码写入配置时难以解决并发问题。

性能问题

打开namespace列表页面,需要显示这个app_id下的所有namespace,因为我们单app_id会存放单个云服务的所有配置,这个量很大,且界面不支持分页,导致页面加载缓慢

体验问题

Apollo的namespace界面未提供搜索功能(可能Apollo设计之初也没想支持这么多),想要从namespace中定位到我们想要查看或修改的namespace,只能借助浏览器的搜索能力。

第三阶段 Apollo与自研配置中心并存

除了上述几个问题,还有一些原因使得我们开始了第三阶段的实践

  • 原来自上而下的配置分层模型,微服务间配置没隔离,不仅不易进行权限管理,而且不适合DevOps 单微服务自治的发布理念。
  • 第二阶段对Apollo改动太多,组织结构变动,没有足够的人力维护
  • 随着集群越来越多,回调功能需要网络的双向打通,网络维护不太方便
  • 我们对Apollo界面以及接口基于业务做的改动较多,导致其他兄弟部门难以共用Apollo

当时大家对是否保留Schema、回调检查、代码写配置这三个功能点有较大的争议。我个人最希望保留Schema、回调检查,因为它们优点显著,而且接口是兼容的,可以与其他部门共用,但是增加了Schema这个概念和回调检查这个流程,会增加学习成本。而代码写配置,由于要解决并发问题,代码改动量较大,我不建议保留。

大家经过激烈的讨论,最终还是废弃了Schema、回调检查、代码写配置这三个功能点,仅仅把运维配置放在Apollo。

然后,我们把业务配置,放在了一个自研的强Schema的配置中心上,这个配置中心,仅负责单集群的配置,每个集群部署一套,满足了我们的业务需求。自研强Schema配置中心的核心要点有,单配置单表、通过注册中心回调来检测配置是否合法、借助mqtt协议来实现长链接推送,无单点瓶颈。

而我们的运维配置中心Apollo回归到了开源的版本,重整了配置的结构,

image-20210405224010878

对运维配置而言好处有

  • 配置模型适合单微服务发布
  • 配置按微服务组织,一个页面上的namespace不会很多

缺点

  • Schema缺失后,不会对操作人员在界面的配置进行校验,即使配置格式或者内容错误也能配置成功。界面上配置密码不支持明文(Apollo无法感知是否为敏感字段),必须提前使用其他工具将明文转换为密文,然后再进行配置。
  • 回调检查功能去掉后,有些配置,如网卡网段配错,操作人员不能即时得到响应

最佳实践

业务配置经过我们的实践,确实不适合使用开源的Apollo。运维配置使用原生的Apollo,但是现在还不具备回调检查和Schema的功能,希望Apollo能在后续版本中支持Schema,或者弱化的json格式检查功能。下面是我们在如下场景下的最佳实践

SRE在界面上的运维配置

通过Apollo来实现功能,至于配置如何组织,根据大家的组织结构、技术架构来对应Apollo上的概念,可按照微服务->部署环境或部署环境 -> 微服务的层级来组织配置

复杂的参数校验

建议在Apollo上面自建portal包裹一层,后端服务可先进行一层处理,这一层处理可以做比较复杂的格式化校验甚至回调检查,再调用Apollo OpenApi将配置写入Apollo。

业务配置的技术选型

最大的挑战是业务配置由用户触发,请求的并发不易处理。思路有两个,一个是在Apollo原生代码的基础上,通过数据库分布式锁来解决并发问题。第二个是借鉴我们的思路,通过单配置单表、mqtt协议实现通知等核心技术点,自研业务配置中心。

业务配置的部署

需要根据业务配置的数量来考虑是否合设业务配置中心。单集群场景下,毫无疑问只需要一个业务配置中心,甚至如果使用Apollo实现,可以考虑和运维配置中心合设。多集群场景下,部署一个业务配置中心,还是多个业务配置中心,我们自己的实践中,一个集群往往要支撑数万用户,我们采取了每个业务集群部署一套业务配置中心的策略。

Kubernetes pod内调用API的流程总体分为以下步骤

  • 创建role
  • 创建serviceaccount
  • 绑定role到serviceaccount
  • 指定pod使用serviceaccount

我们以查pod为例,演示一下整个流程

创建role

1
2
3
4
5
6
7
8
9
10
# role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: role-hzj
namespace: default
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get","list"]
1
kubectl apply -f role.yaml

创建serviceaccount

1
2
3
4
5
6
# serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: serviceaccount-hzj
namespace: default
1
kubectl apply -f serviceaccount.yaml

绑定role

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: rolebinding-hzj
namespace: default
subjects:
- kind: ServiceAccount
name: serviceaccount-hzj
namespace: default
roleRef:
kind: Role
name: role-hzj
apiGroup: rbac.authorization.k8s.io
1
kubectl apply -f rolebinding.yaml

部署pod进行测试

部署一个zookeeper进行测试

手上刚好有zookeeper的模板文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
apiVersion: apps/v1
kind: Deployment
metadata:
name: zookeeper
labels:
app: zookeeper
spec:
replicas: 1
selector:
matchLabels:
app: zookeeper
template:
metadata:
labels:
app: zookeeper
spec:
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
containers:
- name: zookeeper
image: ttbb/zookeeper:stand-alone
imagePullPolicy: IfNotPresent
resources:
limits:
memory: 2G
cpu: 1000m
requests:
memory: 2G
cpu: 1000m
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: PS1
value: '[\u@zookeeper@\W]\$ '

调用API

1
2
3
4
5
6
7
8
9
10
11
12
13
# Point to the internal API server hostname
APISERVER=https://kubernetes.default.svc
# Path to ServiceAccount token
SERVICEACCOUNT=/var/run/secrets/kubernetes.io/serviceaccount
# Read this Pod's namespace
NAMESPACE=$(cat ${SERVICEACCOUNT}/namespace)
# Read the ServiceAccount bearer token
TOKEN=$(cat ${SERVICEACCOUNT}/token)
# Reference the internal certificate authority (CA)
CACERT=${SERVICEACCOUNT}/ca.crt
# Explore the API with TOKEN
curl --cacert ${CACERT} --header "Authorization: Bearer ${TOKEN}" -X GET ${APISERVER}/api
curl --cacert ${CACERT} --header "Authorization: Bearer ${TOKEN}" -X GET ${APISERVER}/api/v1/namespaces/default/pods

kubernetes-pod-api1

发现这里,调用后面的api,403错误。第一个api不报错,是因为该接口不需要鉴权。

修改pod对应的serviceaccount

让我们修改部署模板对应的ServiceAccountName,注入权限。在pod的spec下,设置serviceAccountName

kubernetes-pod-api2

修改部署模板重启后调用api正常

再次尝试上述命令,api结果返回正常

kubernetes-pod-api3

前言

最近因为业务的需求,在学习etcd,想了解一下kubernetes是如何使用etcd集群的,不如动手搭建一个kubernetes集群,也顺手体验一下友商UCloud。后面有时间仔细分析一下kubeadm搭建的时候都做了哪些事情

选择香港Region进行搭建,下载国外的镜像比较方便。

购买三台ECS

基础配置

三台4U16G,基础镜像选择Centos8版本

ApiServer创建ELB

创建LB实例

image-20210413104514588

添加一个6443端口的Vserver

image-20210413104700767

这里Vserver和LVS上的Virtual Service的概念相同。

向6443端口添加Rs

把三台虚拟机的6643端口都添加到负载均衡上

现在6443端口显示异常不要紧,后面安装过程中,各个节点的6443端口才会逐渐可用,让各个节点访问。

初始化Master集群

虚拟机上安装必须组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
yum install -y yum-utils
yum remove -y runc
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
yum install -y docker-ce docker-ce-cli containerd.io
systemctl start docker
cat <<EOF | sudo tee /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-\$basearch
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg
exclude=kubelet kubeadm kubectl
EOF

# Set SELinux in permissive mode (effectively disabling it)
sudo setenforce 0
sudo sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/config

sudo yum install -y kubelet kubeadm kubectl --disableexcludes=kubernetes

sudo systemctl enable --now kubelet
kubeadm config images pull

初始化master-001节点

这里这个IP地址填负载均衡的地址,这样子才能搭建出高可用集群

1
kubeadm init --control-plane-endpoint "10.7.157.12:6443" --upload-certs

初始化成功,返回以下提示信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Your Kubernetes control-plane has initialized successfully!

To start using your cluster, you need to run the following as a regular user:

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

Alternatively, if you are the root user, you can run:

export KUBECONFIG=/etc/kubernetes/admin.conf

You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
https://kubernetes.io/docs/concepts/cluster-administration/addons/

You can now join any number of the control-plane node running the following command on each as root:

kubeadm join 10.7.157.12:6443 --token bj4vpt.o999hbp96p1bvw5q \
--discovery-token-ca-cert-hash sha256:8560fa9211dbfdb55609d22ef0f0b428c6cb73b6e85a70c7a9e13d88b0b8400c \
--control-plane --certificate-key 85c86678b3d54b6017ac3fab2f2a92337f332c7172dfaf4b5a18ee1da679cd7d

Please note that the certificate-key gives access to cluster sensitive data, keep it secret!
As a safeguard, uploaded-certs will be deleted in two hours; If necessary, you can use
"kubeadm init phase upload-certs --upload-certs" to reload certs afterward.

Then you can join any number of worker nodes by running the following on each as root:

kubeadm join 10.7.157.12:6443 --token bj4vpt.o999hbp96p1bvw5q \
--discovery-token-ca-cert-hash sha256:8560fa9211dbfdb55609d22ef0f0b428c6cb73b6e85a70c7a9e13d88b0b8400c

其他节点上执行kubeadm join

1
2
3
kubeadm join 10.7.157.12:6443 --token bj4vpt.o999hbp96p1bvw5q \
--discovery-token-ca-cert-hash sha256:8560fa9211dbfdb55609d22ef0f0b428c6cb73b6e85a70c7a9e13d88b0b8400c \
--control-plane --certificate-key 85c86678b3d54b6017ac3fab2f2a92337f332c7172dfaf4b5a18ee1da679cd7d

调整kubectl命令行

1
2
3
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

安装cni插件

1
kubectl apply -f "https://cloud.weave.works/k8s/net?k8s-version=$(kubectl version | base64 | tr -d '\n')"

至此,kubernetes搭建完成。

image-20210413162451762

前言

华为云IoT服务产品部致力于提供极简接入、智能化、安全可信等全栈全场景服务和开发、集成、托管、运营等一站式工具服务,助力合作伙伴/客户轻松、快速地构建5G、AI万物互联的场景化物联网解决方案。

架构方面,华为云IoT服务产品部采用云原生微服务架构,ZooKeeper组件在华为云IoT服务产品部的架构中扮演着重要的角色,本文将介绍华为云IoT服务产品部在ZooKeeper的使用。

Apache ZooKeeper 简介

Apache ZooKeeper是一个分布式、开源的分布式协调服务,由Apache Hadoop的子项目发展而来。作为一个分布式原语的基石服务,几乎所有分布式功能都可以借助ZooKeeper来实现,例如:应用的主备选举,分布式锁,分布式任务分配,缓存通知,甚至是消息队列、配置中心等。

抛开应用场景,讨论某个组件是否适合,并没有绝对正确的答案。尽管Apache ZooKeeper作为消息队列、配置中心时,性能不用想就知道很差。但是,倘若系统里面只有ZooKeeper,应用场景性能要求又不高,那使用ZooKeeper不失为一个好的选择。但ZooKeeper 客户端的编码难度较高,对开发人员的技术水平要求较高,尽量使用一些成熟开源的ZooKeeper客户端、框架,如:Curator、Spring Cloud ZooKeeper等。

Apache ZooKeeper 核心概念

ZNode

ZNode是ZooKeeper的数据节点,ZooKeeper的数据模型是树形结构,每个ZNode都可以存储数据,同时可以有多个子节点,每个ZNode都有一个路径标识,类似于文件系统的路径,例如:/iot-service/iot-device/iot-device-1。

Apache ZooKeeper在华为云IoT服务产品部的使用

zookeeper-huaweicloud-usage

支撑系统内关键组件

很多开源组件都依赖ZooKeeper,如Flink、Ignite、Pulsar等,通过自建和优化ZooKeeper环境,我们能够为这些高级组件提供更加可靠和高效的服务支持,确保服务的平稳运行。

严格分布式锁

分布式锁是非常常见的需求,相比集群Redis、主备Mysql等,ZooKeeper更容易实现理论上的严格分布式锁。

分布式缓存通知

ZooKeeper的分布式缓存通知能够帮助我们实现分布式缓存的一致性,例如:我们可以在ZooKeeper上注册一个节点,然后在其他节点上监听这个节点,当这个节点发生变化时,其他节点就能够收到通知,然后更新本地缓存。

这种方式的缺点是,ZooKeeper的性能不高,不适合频繁变更的场景,但是,对于一些不经常变更的配置,这种方式是非常适合的。如果系统中存在消息队列,那么可以使用消息队列来实现分布式缓存通知,这种方式的性能会更好、扩展性更强。

分布式Id生成器

直接使用ZooKeeper的有序节点

应用程序可以直接使用ZooKeeper的有序节点来生成分布式Id,但是,这种方式的缺点是,ZooKeeper的性能不高,不适合频繁生成的场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;

import java.util.Optional;

public class ZkDirectIdGenerator {

private ZooKeeper zooKeeper;
private String path = "/zk-direct-id";
private static final String PATH_PREFIX = "/id-";

public ZkDirectIdGenerator(String connectionString, int sessionTimeout) throws Exception {
this.zooKeeper = new ZooKeeper(connectionString, sessionTimeout, event -> {});
initializePath();
}

private void initializePath() throws Exception {
Stat stat = zooKeeper.exists(path, false);
if (stat == null) {
zooKeeper.create(path, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
}

public Optional<String> generateId() {
try {
String fullPath = zooKeeper.create(path + PATH_PREFIX, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL);
return Optional.of(extractId(fullPath));
} catch (Exception e) {
log.error("create znode failed, exception is ", e);
return Optional.empty();
}
}

private String extractId(String fullPath) {
return fullPath.substring(fullPath.lastIndexOf(PATH_PREFIX) + PATH_PREFIX.length());
}
}

使用ZooKeeper生成机器号

应用程序可以使用ZooKeeper生成机器号,然后使用机器号+时间戳+序列号来生成分布式Id。来解决ZooKeeper有序节点性能不高的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import lombok.extern.slf4j.Slf4j;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;

import java.time.LocalDateTime;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

@Slf4j
public class ZkIdGenerator {

private final String path = "/zk-id";

private final AtomicInteger atomicInteger = new AtomicInteger();

private final AtomicReference<String> machinePrefix = new AtomicReference<>("");

private static final String[] AUX_ARRAY = {"", "0", "00", "000", "0000", "00000"};

/**
* 通过zk获取不一样的机器号,机器号取有序节点最后三位
* id格式:
* 机器号 + 日期 + 小时 + 分钟 + 秒 + 5位递增号码
* 一秒可分近10w个id
* 需要对齐可以在每一位补零
*
* @return
*/
public Optional<String> genId() {
if (machinePrefix.get().isEmpty()) {
acquireMachinePrefix();
}
if (machinePrefix.get().isEmpty()) {
// get id failed
return Optional.empty();
}
final LocalDateTime now = LocalDateTime.now();
int aux = atomicInteger.getAndAccumulate(1, ((left, right) -> {
int val = left + right;
return val > 99999 ? 1 : val;
}));
String time = conv2Str(now.getDayOfYear(), 3) + conv2Str(now.getHour(), 2) + conv2Str(now.getMinute(), 2) + conv2Str(now.getSecond(), 2);
String suffix = conv2Str(aux, 5);
return Optional.of(machinePrefix.get() + time + suffix);
}

private synchronized void acquireMachinePrefix() {
if (!machinePrefix.get().isEmpty()) {
return;
}
try {
ZooKeeper zooKeeper = new ZooKeeper(ZooKeeperConstant.SERVERS, 30_000, null);
final String s = zooKeeper.create(path, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL);
if (s.length() > 3) {
machinePrefix.compareAndSet("", s.substring(s.length() - 3));
}
} catch (Exception e) {
log.error("connect to zookeeper failed, exception is ", e);
}
}

private static String conv2Str(int value, int length) {
if (length > 5) {
throw new IllegalArgumentException("length should be less than 5");
}
String str = String.valueOf(value);
return AUX_ARRAY[length - str.length()] + str;
}

}

微服务注册中心

相比其他微服务引擎,如阿里云的MSE、Nacos等,已有的Zookeeper集群作为微服务的注册中心,既能满足微服务数量较少时的功能需求,并且更加节约成本

数据库连接均衡

在此前的架构中,我们采用了一种随机策略来分配微服务与数据库的连接地址。下图展示了这种随机分配可能导致的场景。考虑两个微服务:微服务B和微服务C。尽管微服务C的实例较多,但其对数据库的操作相对较少。相比之下,微服务B在运行期间对数据库的操作更为频繁。这种连接方式可能导致数据库Data2节点的连接数和CPU使用率持续居高,从而成为系统的瓶颈。

zookeeper-database-before.png

启发于Kafka中的partition分配算法,我们提出了一种新的连接策略。例如,如果微服务B1连接到了Data1和Data2节点,那么微服务B2将连接到Data3和Data4节点。如果存在B3实例,它将再次连接到Data1和Data2节点。对于微服务C1,其连接将从Data1和Data2节点开始。然而,由于微服务的数量与数据库实例数量的两倍(每个微服务建立两个连接)并非总是能整除,这可能导致Data1和Data2节点的负载不均衡。

为了解决这一问题,我们进一步优化了策略:第一个微服务实例在选择数据库节点时,将从一个随机起点开始。这种方法旨在确保Data1和Data2节点的负载均衡。具体的分配策略如下图所示。

zookeeper-database-after.png

Apache ZooKeeper在华为云IoT产品部的部署/运维

服务端部署方式

我们所有微服务和中间件均采用容器化部署,选择3节点(没有learner)规格。使用statefulset和PVC的模式部署。为什么使用statefulset进行部署?statefulset非常适合用于像Zookeeper这样有持久化存储需求的服务,每个Pod可以和对应的存储资源绑定,保证数据的持久化,同时也简化了部署,如果想使用deploy的部署模式,需要规划、固定每个pod的虚拟机部署。

Zookeeper本身对云硬盘的要求并不高,普通IO,几十G存储就已经能够支撑Zookeeper平稳运行了。Zookeeper本身运行的资源,使用量不是很大,在我们的场景,规格主要取决于Pulsar的topic数量,如果Pulsar的topic不多,那么0.5核、2G内存已经能保证Zookeeper平稳运行了。

客户端连接方式

借助coredns,客户端使用域名的方式连接Zookeeper,这样可以避免Zookeeper的IP地址变更导致客户端连接失败的问题,如zookeeper-0.zookeeper:2181,zookeeper-1.zookeeper:2181,zookeeper-2.zookeeper:2181

重要监控指标

  • readlantency、updatelantency

    zk的读写延迟

  • approximate_data_size

    zk中数据的平均大小估计

  • outstanding_requests

    等待Zookeeper处理的请求数

  • znode_count

    Zookeeper当前的znode总数

  • num_alive_connections

    Zookeeper当前活跃的连接数

Apache ZooKeeper在华为云IoT产品部的问题

readiness合理设置

这是碰到的最有趣的问题,readiness接口是k8s判断pod是否正常的依据,那么对于Zookeeper集群来说,最合理的就是,当这个Zookeeper节点加入集群,获得了属于自己的Leader或Follower状态,就算pod正常。可是,当初次部署的时候,只有一个节点可用,该节点一个实例无法完成选举流程,导致无法部署。

综上,我们把readiness的策略修改为:

zookeeper-readiness-strategy.png

PS:为了让readiness检查不通过时,Zookeeper集群也能选主成功,需要配置publishNotReadyAddresses为true,示例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: v1
kind: Service
metadata:
name: zookeeper
spec:
selector:
app: zookeeper
clusterIP: None
sessionAffinity: None
publishNotReadyAddresses: true
ports:
- protocol: TCP
port: 2181
name: client
- protocol: TCP
port: 2888
name: peer
- protocol: TCP
port: 3888
name: leader

jute.maxbuffer超过上限

jute.maxbuffer,这个是znode中存储数据大小的上限,在客户端和服务端都需要配置,根据自己在znode上存储的数据合理配置

zookeeper的Prometheus全0监听

不满足网络监听最小可见原则。修改策略,添加一个可配置参数来配置监听的IP metricsProvider.httpHost,PR已合入,见 https://github.com/apache/zookeeper/pull/1574/files

客户端版本号过低,域名无法及时刷新

客户端使用域名进行连接,但在客户端版本号过低的情况下,客户端并不会刷新新的ip,还是会用旧的ip尝试连接。升级客户端版本号到curator-4.3.0以上、zookeeper-3.6.2以上版本后解决。

总结

本文详细介绍了华为云IoT服务产品部如何使用Apache ZooKeeper来优化其云原生微服务架构。ZooKeeper作为分布式协调服务,在华为云IoT服务中发挥了重要作用,用于主备选举、分布式锁、任务分配和缓存通知等。文中还讨论了ZooKeeper在分布式ID生成、微服务注册中心、数据库连接均衡等方面的应用。此外,文章还覆盖了ZooKeeper在华为云IoT产品部的部署、运维策略和所遇到的挑战,包括容器化部署、监控指标和配置问题。

前言

记一次代码检视中领悟到的知识,和大家一起交流

正文

提交上来的代码大概是这个样子的

1
2
3
4
5
Socket socket = new Socket(ip, port);
final DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
dataOutputStream.write("HelloWorld".getBytes(StandardCharsets.UTF_8));
socket.shutdownOutput();
dataOutputStream.flush();

这次主要是添加shutdownOutput的调用,及时关闭tcp会话,防止TW过多。

经过大家的讨论,主要的矛盾点在shutdownOutput和flush的顺序。首先想到的是 flush方法放在了output后面,这样还能起作用吗?但是提交代码之前是经过测试的,这样子是可以正常工作的。然后的想法就是,傻逼了,想错了,shutdown应该自带flush效果,os都发fin了,之前的buffer肯定出去了。

我做个实验,来探究下是不是这样子的,我从python开启了一个http server来开启实验

1
python3 -m http.server

java测试类代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.github.hezhangjian.demo.basic;

import lombok.extern.slf4j.Slf4j;
import org.junit.Test;

import java.io.DataOutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

/**
* @author hezhangjian
*/
@Slf4j
public class DemoSocketSend {

String ip = "127.0.0.1";

int port = 8000;

@Test
public void testSocketSend() throws Exception {
Socket socket = new Socket(ip, port);
final DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
dataOutputStream.write("HelloWorld".getBytes(StandardCharsets.UTF_8));
socket.shutdownOutput();
dataOutputStream.flush();
}

}

随后我在25行和26行打了断点

当运行到25行的时候,python server并没有收到数据

image-20210402121759024

还没运行26行的时候,数据就已经发送到python服务器了

image-20210402121818000

总结

这个时候证明我们的推测是正确的,shutdownOutput方法自带了flush效果。
我也尝试了配置tcpNoDeplay参数,配不配置tcpNoDelay,都是一样的效果。看起来jvm都有缓冲
那么已经调用了shutdownOutput方法之后,flush方法还有没有必要调用呢,从clean code的角度,flush方法的调用已经是没有任何必要的了,建议删除。一般场景下可能不会有问题,但是如果极端场景,比如在25行到26行之间,程序陷入了长gc,这行就有可能抛出IOException,影响原来的逻辑。

背景

我们的业务有些时候总是在升级期间rpc业务有一些呼损,想总结一下让rpc调用零呼损的两种方式:重试和优雅启停。我先介绍这两种方式,再描述一下这两种方式的优缺点

rpc-lossless

A是一个微服务

B也是一个微服务

蓝色的是常见的注册中心,有zookeeper、eureka等实现。

重试

重试,在发生可重试错误的时候,重试一次。什么是可重试错误呢?就是重试一次,可能会成功。比如400 BadRequest,那出现这种错误,基本上重试也没有用,就不要浪费我们宝贵的服务器资源了。常见的如servicecomb框架就有重试几次、重试间隔这样的参数。值得一提的是,如果你指望通过重试让升级零呼损,那么你的重试次数,要比你的并行升级实例数大才行。

这也很容易理解,比如A服务调用B服务,B服务有5个实例,B1~B5。这个时候,同时升级B1和B2,A第一次调用了B1,接下来重试,如果运气不好,恰好重试到了B2节点,那么业务还是会失败的。如果防异常故障,就得重试三次才行。

如果是防止单数据中心宕机,重试次数大于同时宕机节点数,这个规则可能就没那么靠谱了。现在,企业部署十几个乃至二十几个微服务实例,已经不是什么新闻了,假设分3数据中心部署,总不能重试接近10次吧,这种时候,最好重试策略和数据中心相关,重试的时候,选择另一个az的实例。目前servicecomb还不支持这种功能。

优雅启停

优雅停止

优雅停止,就是说当微服务快要宕机的时候,先从注册中心进行去注册,然后把发送给微服务的消息,处理完毕后,再彻底关闭。这个方式,可以有效地防止升级期间,发送到老节点的呼损。

优雅启动

优雅启动,当微服务实例,能够处理rpc请求的时候,再将实例自己注册到注册中心。避免请求发进来,实例却无法处理。

这里有一个要求,就是调用方发现被调用方(即A发现B)的注册中心,要和B注册、去注册的注册中心是一个注册中心。有案例是,发现采用k8s发现,注册、去注册却使用微服务引擎,导致呼损。

优劣对比

可预知节点升级的场景

重试相对于优雅启停,在预知节点升级的场景没那么优雅,重试次数可能还要和并行升级的节点挂钩,非常的不优雅,且难以维护

不可预知节点升级的场景

优雅启停无法对不可预知节点升级的场景生效。只有重试能在这个场景发挥作用

其他场景

重试可以很好地处理网络闪断、长链接中断等场景

总结

想要实现rpc调用零呼损,重试和优雅启停都不可或缺,都需要实现。

Dapper出现的背景

分布式系统不容易观测。有些问题靠日志和统计根本无法挖掘。
有些无法重现或极难重现的场景。

Dapper设计的原则

低时延

微不足道的性能影响,使应用程序团队愿意迁移。

应用透明

应用尽量少做侵入式修改

可扩展

随着应用程序的规模扩展

Dapper概念

概览

image-20210321221020253

image-20210321214734150

通过引入parent id和span id等来将调用链串起来

trace id

特定的模式是trace id

span id

span,包括日志,起始、终止时间,也包括key、value。还区分了网络和非网络时延。

annotation

应用程序根据自己的需要可以打上annotation,不仅仅是key、value,还可以有时间戳等,有助于分析方法级别的耗时

image-20210321221204695

Dapper原理

  • 每当一个线程处理一个采样的控制线路时。Dapper在其中放置一个trace context在thread-local中。trace
    context是一个小型、容易拷贝的对象,包含trace id和span id
  • 异步或者callback的时候,用统一的library封装传递
  • RPC自动继承
  • 不行,就通过api的方式接入

Dapper的流程

image-20210321215159560

写入流程分为三步。1、写入本地日志文件 2、被Dapper daemon获取 3、写入Bigtable

端到端的中位数时延在15秒。百分之75的数据的98时延在2分钟以内,但百分之25的数据的98时延可能会到几小时

为什么不在RPC接口中顺手收集信息

  • trace信息可能比rpc本身的报文要大
  • 对于异步流程,无法收集

Trace消耗的性能

在应用程序侧

创建root span耗时约204纳秒,非root span耗时176纳秒

日志文件的消耗,可并行批量

image-20210321215727742

Trace收集

每个trace数据约426byte

仅占用千分之一的Google生产带宽

自适应采样

测试环境或低量请求多采样。请求量大小采样

远端采样

远端采样,降低服务端风险。

Dapper索引选择

基于服务、机器、时间

Dapper的典型使用流程

  • 1用户选择服务、时间、期望看到的指标值(如时延)
  • 2展示出满足这个条件的所有pattern,(trace pattern)
  • 3展示这个pattern的调用链图,
  • 4右侧是全部的采样
  • 5展示出详细的时间戳分布

Dapper实际作用

  • 发现无意义的调用
  • 是否有必要访问主库,而不是从库
  • 理解黑盒系统的依赖
  • 测试。如果你不能观测,就不能优化
  • 推断依赖
  • 判断点到点网络故障,根据请求的大小,进一步推断,谁到谁引发了网络故障。
  • 在底层share系统之上,外围按用户或其他维度统计调用
  • Dapper daemon的实时信息,可用来救火

Dapper现在做不到的

  • 合并、批量处理,无法拆开识别
  • 针对批系统,能力有限
  • 根因分析,需要联合分析,比如在annotation中携带线程池队列大小,进行多个请求的联合分析等
  • 如何和Linux内核关联,如何将错误和内核联系起来
0%