背景

最近公司将我们之前使用的链路工具切换为了 OpenTelemetry.

我们的技术栈是:

        OTLP
Client──────────►Collect────────►StartRocks
(Agent) ▲


Jaeger

其中客户端使用 OpenTelemetry 提供的 Java Agent 进行埋点收集数据,再由 Agent 通过 OTLP(OpenTelemetry Protocol) 协议将数据发往 Collector,在 Collector 中我们可以自行任意处理数据,并决定将这些数据如何存储(这点在以往的 SkyWalking 体系中是很难自定义的)

这里我们将数据写入 StartRocks 中,供之后的 UI 层进行查看。

OpenTelemetry 是可观测系统的新标准,基于它可以兼容以前使用的 Prometheus、 victoriametrics、skywalking 等系统,同时还可以灵活扩展,不用与任何但一生态或技术栈进行绑定。

更多关于 OTel 的内容会在今后介绍。

难点

其中有一个关键问题就是:如何在线上进行无缝切换

虽然我们内部的发布系统已经支持重新发布后就会切换到新的链路,也可以让业务自行发布然后逐步的切换到新的系统,这样也是最保险的方式。

但这样会有几个问题:

  • 当存在调用依赖的系统没有全部切换为新链路时,再查询的时候就会出现断层,整个链路无法全部串联起来。
  • 业务团队没有足够的动力去推动发布,可能切换的周期较长。

所以最好的方式还是由我们在后台统一发布,对外没有任何感知就可以一键全部切换为 OpenTelemetry。

仔细一看貌似也没什么难的,无非就是模拟用户点击发布按钮而已。

但这事由我们自动来做就不一样了,用户点击发布的时候会选择他们认为可以发布的分支进行发布,我们不能自作主张的比如选择 main 分支,有可能只是合并了但还不具备发布条件。

所以保险的方式还是得用当前项目上一次发布时所使用的 git hash 值重新打包发布。

但这也有几个问题:

  • 重复打包发布太慢了,线上几十上百个项目,每打包发布一次就得几分钟,虽然可以并发,但考虑到 kubernetes 的压力也不能调的太高。
  • 保不准业务镜像中有单独加入一些环境变量,这样打包可能会漏。

切换方案

所以思来想去最保险的方法还是将业务镜像拉取下来,然后手动删除镜像中的 skywalking 包以及 JVM 参数,全部替换为 OpenTelemetry 的包和 JVM 参数。

整体的方案如下:

  1. 遍历 namespace 的 pod >0 的 deployment
  2. 遍历 deployment 中的所有 container,获得业务镜像
    1. 跳过 istio 和日志采集 container,获取到业务容器
    2. 判断该容器是否需要替换,其实就是判断环境变量中是否有 skywalking ,如果有就需要替换。
    3. 获取业务容器的镜像
  3. 基于该 Image 重新构建一个 OpenTelemetry 的镜像

    3.1 新的镜像包含新的启动脚本.

    3.1.1 新的启动脚本中会删除原有的 skywalking agent

    3.2 新镜像会包含 OpenTelemetry 的 jar 包以及我们自定义的 OTel 扩展包

    3.3 替换启动命令为新的启动脚本
  4. 修改 deployment 中的 JVM 启动参数
  5. 修改 deployment 的镜像后滚动更新
  6. 开启一个 goroutine 定时检测更新之后是否启动成功
    1. 如果长时间 (比如五分钟) 都没有启动成功,则执行回滚流程

具体代码

因为需要涉及到操作 kubernetes,所以整体就使用 Golang 实现了。

遍历 deployment 得到需要替换的容器镜像

func ProcessDeployment(ctx context.Context, finish []string, deployment v1.Deployment, clientSet kubernetes.Interface) error {
deploymentName := deployment.Name
for _, s := range finish {
if s == deploymentName {
klog.Infof("Skip finish deployment:%s", deploymentName)
return nil
}
}
// Write finish deployment name to a file
defer writeDeploymentName2File(deploymentName, fmt.Sprintf("finish-%s.log", deployment.Namespace)) appName := deployment.GetObjectMeta().GetLabels()["appName"]
klog.Infof("Begin to process deployment:%s, appName:%s", deploymentName, appName) upgrade, err := checkContainIstio(ctx, deployment, clientSet)
if err != nil {
return err
}
if upgrade == false {
klog.Infof("Don't have istio, No need to upgrade deployment:%s appName:%s", deploymentName, appName)
return nil
} for i, container := range deployment.Spec.Template.Spec.Containers {
if strings.HasPrefix(deploymentName, container.Name) { // Check if container has sw jvm
for _, envVar := range container.Env {
if envVar.Name == "CATALINA_OPTS" {
if !strings.Contains(envVar.Value, "skywalking") {
klog.Infof("Skip upgrade don't have sw jvm deployment:%s container:%s", deploymentName, container.Name)
return nil
}
}
}
upgrade(container) // Check newDeployment status
go checkNewDeploymentStatus(ctx, clientSet, newDeployment) // delete from image
deleteImage(container.Image) }
} return nil
}

这个函数需要传入一个 deployment ,同时还有一个已经完成了的列表进来。

已完成列表用于多次运行的时候可以快速跳过已经执行的 deployment。

checkContainIstio() 函数很简单,判断是否包含了 Istio 容器,如果没有包含说明不是后端应用(可能是前端、大数据之类的任务),就可以直接跳过了。




而判断是否需要替换的前提这事判断环境变量 CATALINA_OPTS 中是否包含了 skywalking 的内容,如果包含则说明需要进行替换。

Upgrade 核心函数

func upgrade(container Container){
klog.Infof("Begin to upgrade deployment:%s container:%s", deploymentName, container.Name)
newImageName := fmt.Sprintf("%s-otel-%s", container.Image, generateRandomString(4))
err := BuildNewOtelImage(container.Image, newImageName)
if err != nil {
return err
} // Update deployment jvm ENV
for e, envVar := range container.Env {
if envVar.Name == "CATALINA_OPTS" {
otelJVM := replaceSWAgent2OTel(envVar.Value, appName)
deployment.Spec.Template.Spec.Containers[i].Env[e].Value = otelJVM
}
}
// Update deployment image
deployment.Spec.Template.Spec.Containers[i].Image = newImageName newDeployment, err := clientSet.AppsV1().Deployments(deployment.Namespace).Update(ctx, &deployment, metav1.UpdateOptions{})
if err != nil {
return err
}
klog.Infof("Finish upgrade deployment:%s container:%s", deploymentName, container.Name)
}

这里一共分为以下几部:

  • 基于老镜像构建新镜像
  • 更新原有的 CATALINA_OPTS 环境变量,也就是替换 skywalking 的参数
  • 更新 deployment 镜像,触发滚动更新

构建新镜像

	dockerfile = fmt.Sprintf(`FROM %s
COPY %s /home/admin/%s
COPY otel.tar.gz /home/admin/otel.tar.gz
RUN tar -zxvf /home/admin/otel.tar.gz -C /home/admin
RUN rm -rf /home/admin/skywalking-agent
ENTRYPOINT ["/bin/sh", "/home/admin/start.sh"]
`, fromImage, script, script) idx := strings.LastIndex(newImageName, "/") + 1
dockerFileName := newImageName[idx:]
create, err := os.Create(fmt.Sprintf("Dockerfile-%s", dockerFileName))
if err != nil {
return err
}
defer func() {
create.Close()
os.Remove(create.Name())
}()
_, err = create.WriteString(dockerfile)
if err != nil {
return err
} cmd := exec.Command("docker", "build", ".", "-f", create.Name(), "-t", newImageName)
cmd.Stdin = strings.NewReader(dockerfile)
if err := cmd.Run(); err != nil {
return err
}

其实这里的重点就是构建这个新镜像,从这个 dockerfile 中也能看出具体的逻辑,也就是上文提到的删除原有的 skywalking 资源同时将新的 OpenTelemetry 资源打包进去。

最后再将这个镜像上传到私服。



其中的替换 JVM 参数也比较简单,直接删除 skywalking 的内容,然后再追加上 OpenTelemetry 需要的参数即可。

定时检测替换是否成功

func checkNewDeploymentStatus(ctx context.Context, clientSet kubernetes.Interface, newDeployment *v1.Deployment) error {
ready := true
tick := time.Tick(10 * time.Second)
for i := 0; i < 30; i++ {
<-tick
originPodList, err := clientSet.CoreV1().Pods(newDeployment.Namespace).List(ctx, metav1.ListOptions{
LabelSelector: metav1.FormatLabelSelector(&metav1.LabelSelector{
MatchLabels: newDeployment.Spec.Selector.MatchLabels,
}),
})
if err != nil {
return err
} // Check if there are any Pods
if len(originPodList.Items) == 0 {
klog.Infof("No Pod in deployment:%s, Skip", newDeployment.Name)
}
for _, item := range originPodList.Items {
// Check Pod running
for _, status := range item.Status.ContainerStatuses {
if status.RestartCount > 0 {
ready = false
break
}
}
}
klog.Infof("Check deployment:%s namespace:%s status:%t", newDeployment.Name, newDeployment.Namespace, ready)
if ready == false {
break
}
} if ready == false {
// rollback
klog.Infof("=======Rollback deployment:%s namespace:%s", newDeployment.Name, newDeployment.Namespace)
writeDeploymentName2File(newDeployment.Name, fmt.Sprintf("rollback-%s.log", newDeployment.Namespace))
} return nil
}

这里会启动一个 10s 执行一次的定时任务,每次都会检测是否有容器发生了重启(正常情况下是不会出现重启的)

如果检测了 30 次都没有重启的容器,那就说明本次替换成功了,不然就记录一个日志文件,然后人工处理。

这种通常是原有的镜像与 OpenTelemetry 不兼容,比如里面写死了一些 skywalking 的 API,导致启动失败。

所以替换任务跑完之后我还会检测这个 rollback-$namespace 的日志文件,人工处理这些失败的应用。

分批处理 deployment

最后讲讲如何单个调用刚才的 ProcessDeployment() 函数。

考虑到不能对 kubernetes 产生影响,所以我们需要限制并发处理 deployment 的数量(我这里的限制是 10 个)。

所以就得分批进行替换,每次替换 10 个,而且其中有一个执行失败就得暂停后续任务,由人工检测失败原因再决定是否继续处理。

毕竟处理的是线上应用,需要小心谨慎。

所以触发的代码如下:

func ProcessDeploymentList(ctx context.Context, data []v1.Deployment, clientSet kubernetes.Interface) error {
file, err := os.ReadFile(fmt.Sprintf("finish-%s.log", data[0].Namespace))
if err != nil {
return err
}
split := strings.Split(string(file), "\n") batchSize := 10
start := 0 for start < len(data) { end := start + batchSize
if end > len(data) {
end = len(data)
} batch := data[start:end] //等待goroutine结束
var wg sync.WaitGroup
klog.Infof("Start process batch size %d", len(batch)) errs := make(chan error, len(batch)) wg.Add(len(batch))
for _, item := range batch {
d := item
go func() {
defer wg.Done()
if err := ProcessDeployment(ctx, split, d, clientSet); err != nil {
klog.Errorf("!!!Process deployment name:%s error: %v", d.Name, err)
errs <- err
return
}
}()
} go func() {
wg.Wait()
close(errs)
}() //任何一个失败就返回
for err := range errs {
if err != nil {
return err
}
} start = end
klog.Infof("Deal next batch")
} return nil }

使用 WaitGroup 来控制一组任务,使用一个 chan 来传递异常;这类分批处理的代码在一些批处理框架中还蛮常见的。

总结

最后只需要查询某个 namespace 下的所有 deployment 列表传入这个批处理函数即可。

不过整个过程中还是有几个点需要注意:

  • 因为需要替换镜像的前提是要把现有的镜像拉取到本地,所以跑这个任务的客户端需要有充足的磁盘,同时和镜像服务器的网络条件较好。
  • 不然执行的过程会比较慢,同时磁盘占用满了也会影响任务。

其实这个功能依然有提升空间,考虑到后续会升级 OpenTelemetry agent 的版本,甚至也需要增减一些 JVM 参数。

所以最后有一个统一的工具,可以直接升级 Agent,而不是每次我都需要修改这里的代码。

后来在网上看到了得物的相关分享,他们可以远程加载配置来解决这个问题。

这也是一种解决方案,直到我们看到了 OpenTelemetry 社区提供了 Operator,其中也包含了注入 agent 的功能。

apiVersion: opentelemetry.io/v1alpha1
kind: Instrumentation
metadata:
name: my-instrumentation
spec:
exporter:
endpoint: http://otel-collector:4317
propagators:
- tracecontext
- baggage
- b3
sampler:
type: parentbased_traceidratio
argument: "0.25"
java:
image: private/autoinstrumentation-java:1.32.0-1

我们可以使用他提供的 CRD 来配置我们 agent,只要维护好自己的镜像就好了。

使用起来也很简单,只要安装好了 OpenTelemetry-operator ,然后再需要注入 Java Agent 的 Pod 中使用注解:

instrumentation.opentelemetry.io/inject-java: "true"

operator 就会自动从刚才我们配置的镜像中读取 agent,然后复制到我们的业务容器。

再配置上环境变量 $JAVA_TOOL_OPTIONS=/otel/javaagent.java, 这是一个 Java 内置的环境变量,应用启动的时候会自动识别,这样就可以自动注入 agent 了。

envJavaToolsOptions   = "JAVA_TOOL_OPTIONS"

// set env value
idx := getIndexOfEnv(container.Env, envJavaToolsOptions)
if idx == -1 {
container.Env = append(container.Env, corev1.EnvVar{
Name: envJavaToolsOptions,
Value: javaJVMArgument,
})} else {
container.Env[idx].Value = container.Env[idx].Value + javaJVMArgument
} // copy javaagent.jar
pod.Spec.InitContainers = append(pod.Spec.InitContainers, corev1.Container{
Name: javaInitContainerName,
Image: javaSpec.Image,
Command: []string{"cp", "/javaagent.jar", javaInstrMountPath + "/javaagent.jar"},
Resources: javaSpec.Resources,
VolumeMounts: []corev1.VolumeMount{{
Name: javaVolumeName,
MountPath: javaInstrMountPath,
}},})

大致的运行原理是当有 Pod 的事件发生了变化(重启、重新部署等),operator 就会检测到变化,此时会判断是否开启了刚才的注解:

instrumentation.opentelemetry.io/inject-java: "true"

接着会写入环境变量 JAVA_TOOL_OPTIONS,同时将 jar 包从 InitContainers 中复制到业务容器中。

这里使用到了 kubernetes 的初始化容器,该容器是用于做一些准备工作的,比如依赖安装、配置检测或者是等待其他一些组件启动成功后再启动业务容器。

目前这个 operator 还处于使用阶段,同时部分功能还不满足(比如支持自定义扩展),今后有时间也可以分析下它的运行原理。

参考链接:

实战:如何优雅的从 Skywalking 切换到 OpenTelemetry的更多相关文章

  1. 小白学jquery Mobile《构建跨平台APP:jQuery Mobile移动应用实战》连载四(场景切换)

    作为一款真正有使用价值的应用,首先应该至少有两个页面,通过页面的切换来实现更多的交互.比如手机人人网,打开以后先是进入登录页面,登录后会有新鲜事,然后拉开左边的面板,能看到相册.悄悄话.应用之类的其他 ...

  2. Android项目实战(四):ViewPager切换动画(3.0版本以上有效果)

    学习内容来自“慕课网” 一般APP进去之后都会有几张图片来导航,这里就学习怎么在这张图片切换的时候添加切换动画效果 先看布局文件 activity_main.layout <?xml versi ...

  3. python实战===如何优雅的打飞机

    这是一个打飞机的游戏,结构如下: 其中images中包含的素材为 命名为alien.png    命名为ship.png 游戏效果运行是这样的: 敌军,也就是体型稍微大点的,在上方左右移动,并且有规律 ...

  4. 《JavaScript 实战》:JavaScript 图片滑动切换效果

    看到alibaba的一个图片切换效果,感觉不错,想拿来用用.但代码一大堆的,看着昏,还是自己来吧.由于有了做图片滑动展示效果的经验,做这个就容易得多了. 效果预览 仿淘宝/alibaba图片切换: 默 ...

  5. Android实战技巧之八:Ubuntu下切换JDK版本【转】

    本文转载自:http://blog.csdn.net/lincyang/article/details/42024565 Android L之后推荐使用JDK7编译程序,这是自然发展规律,就像是4年前 ...

  6. 【赶快收藏】Hystrix实战,优雅提升系统的鲁棒性

    背景 最近接手了一个系统,其功能都是查询.查询分了两种方式,一种是公司集团提供的查询能力,支持全国各个省份的查询,但是业务高峰期时服务响应比较慢:另外一种是各省的分公司都分别提供了对应的查询能力,但是 ...

  7. Kafka实战(七) - 优雅地部署 Kafka 集群

    既然是集群,必然有多个Kafka节点,只有单节点构成的Kafka伪集群只能用于日常测试,不可能满足线上生产需求. 真正的线上环境需要考量各种因素,结合自身的业务需求而制定.看一些考虑因素(以下顺序,可 ...

  8. Selenium 2自动化测试实战15(多表单切换)

    一.多表单切换 在web应用中经常会遇到frame/iframe表单嵌套页面的应用,WebDriver只能在一个页面上对元素识别与定位,对于frame/iframe表单内嵌页面上的元素无法直接定位.这 ...

  9. Scala零基础教学【102-111】Akka 实战-深入解析

    第102讲:通过案例解析Akka中的Actor运行机制以及Actor的生命周期 Actor是构建akka程序的核心基石,akka中actor提供了构建可伸缩的,容错的,分布式的应用程序的基本抽象, a ...

  10. Docker-Compose基础与实战,看这一篇就够了

    what & why Compose 项目是 Docker 官方的开源项目,负责实现对 Docker 容器集群的快速编排.使用前面介绍的Dockerfile我们很容易定义一个单独的应用容器.然 ...

随机推荐

  1. ZYNQ 裸机模式下修改默认uart端口

    ## 背景 调试ZYNQ 裸机code, 调用 printf()后在UART端口无法看到打印信息输出,查看原理图后发现,板子用的UART 1作为默认串口调试接口,UART 0分配给了RS485使用,因 ...

  2. linux的简单使用

    了解Linux的简单使用 Linux的安装 下载Linux Ubuntu版本和虚拟机VMware软件. 我已经提前下载好了,下载好的文件分享出来bd 这个是文件夹内的VMWare软件的注册码,安装完成 ...

  3. Android---Android 开发四大组件

    Android 应用程序组件 应用程序组件是一个Android应用程序的基本构建块.这些组件由应用清单文件松耦合的组织.AndroidManifest.xml描述了应用程序的每个组件,以及他们如何交互 ...

  4. Java 封装性的四种权限测试 + 总结

    *    总结封装性:Java提供了4中权限修饰符来修饰类及类的内部结构,体现类及类的内部结构再被调用时的可见性的大小 1 package com.bytezero.circle; 2 3 publi ...

  5. 9、mysql的并发参数调整

    从实现上来说,MySQL Server 是多线程结构,包括后台线程和客户服务线程.多线程可以有效利用服务器资源,提高数据库的并发性能.在Mysql中,控制并发连接和线程的主要参数包括 max_conn ...

  6. .Net下的CORS跨域设置

    CORS跨域访问问题往往出现在"浏览器客户端"通过ajax调用"服务端API"的时候.而且若是深究原理,还会发现跨域问题其实还分为[简单跨域]与[复杂跨域]这两 ...

  7. 尚硅谷Java 宋红康2023版 - 学习笔记

    尚硅谷Java 宋红康2023版 - 学习笔记 观看地址 https://www.bilibili.com/video/BV1PY411e7J6 60-IDEA开发工具-HelloWorld的编写与相 ...

  8. AutoNumber VsCode插件开发

    AutoNumber VsCode插件开发 ::: details 目录 目录 AutoNumber VsCode插件开发 Step. 2: 安装脚手架 Step. 3: 创建空项目 Step. 4: ...

  9. 可穿戴心电ECG监测的技术路径及特点

    在传统的医疗设备中,监测心跳速率和心脏活动是经由测量电生理讯号与心电图 (ECG) 来完成的,需要将电极连接到身体来量测心脏组织中所引发电气活动的信号.常见的设备用医院的心电图机,长期监护的动态心电仪 ...

  10. day02-显示所有菜品&点餐功能

    满汉楼02 4.功能实现04 4.6显示所有菜品 4.6.1思路分析 创建一个菜单表menu,在Domain层创建与菜单表对应的Javabean-Menu类,在DAO层创建MenuDAO,完成对men ...