热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

kubernets轻量containlog日志收集技巧

首先这里要收集的日志是容器的日志,而不是集群状态的日志要完成的三个点,收集,监控,报警,收集是基础,监控和报警可以基于收集的日志来作,这篇主要实现收集不想看字的可以直接看代码,一共

首先这里要收集的日志是容器的日志,而不是集群状态的日志

 

要完成的三个点,收集,监控,报警,收集是基础,监控和报警可以基于收集的日志来作,这篇主要实现收集

 

不想看字的可以直接看代码,一共没几行,尽量用调用本地方法实现,有工夫的可以改写成shell脚本

https://github.com/cclient/kubernetes-filebeat-collector

官方的收集方案

https://kubernetes.io/docs/tasks/debug-application-cluster/logging-elasticsearch-kibana/

 

一些基础的总结方案,文章时间比较早

https://jimmysong.io/kubernetes-handbook/practice/app-log-collection.html

 

官方的方案限制较多,因各种原因放弃

文内的原因是

```

Kubernetes官方提供了EFK的日志收集解决方案,但是这种方案并不适合所有的业务场景,它本身就有一些局限性,例如:

  • 所有日志都必须是out前台输出,真实业务场景中无法保证所有日志都在前台输出
  • 只能有一个日志输出文件,而真实业务场景中往往有多个日志输出文件
  • Fluentd并不是常用的日志收集工具,我们更习惯用logstash,现使用filebeat替代
  • 我们已经有自己的ELK集群且有专人维护,没有必要再在kubernetes上做一个日志收集服务

基于以上几个原因,我们决定使用自己的ELK集群。

```

个人不采用官方收集的原因基本类似,既要收集out前台输出,也要收集映射目录的多个输出文件,个人也不喜欢Fluentd,太重,整体较封闭。

 

基本实现方式

编号 方案 优点 缺点
1 每个app的镜像中都集成日志收集组件 部署方便,kubernetes的yaml文件无须特别配置,可以为每个app自定义日志收集配置 强耦合,不方便应用和日志收集组件升级和维护且会导致镜像过大
2 单独创建一个日志收集组件跟app的容器一起运行在同一个pod中 低耦合,扩展性强,方便维护和升级 需要对kubernetes的yaml文件进行单独配置,略显繁琐
3 将所有的Pod的日志都挂载到宿主机上,每台主机上单独起一个日志收集Pod 完全解耦,性能最高,管理起来最方便 需要统一日志收集规则,目录和输出方式

文内的需求选择的是方案2

 

文内和我的需有区别,但不管需求如何

 

方案1,2都引入了更多的复杂度,需要较多的额外工作,区别只是引入时机,1是build image时和2是deploy时

 

1,2从设计上就可以排除,太不优雅,以开发语言来说,侵入性太高,3是类似aop性质的能减少侵入,只有在3难以实现的情况下才考虑,实际上3并不难实现,早前纯docker集群就部署过类似的方案,稍改改就能适用于kubernetes

 

而且这些方案的设计,都只考虑到了,收集的是自定义日志文件,映射外部目录下的文件,不论在容器内,容器外读取,都只监听相应目录下的文件,都不收集 docker 默认的json file,即 docker logs --tail 100 -f contain_name 这类日志的数据

 

docker logs 操作上比较方便,早先设计docker 日志收集时,虽官方支持不少日志插件

因为`The docker logs command is not available for drivers other than json-file and journald.`

https://docs.docker.com/config/containers/logging/configure/#supported-logging-drivers

 

过去一直采用默认的json-file,收集时,直接监听 /var/lib/docker/containers/*/*-json.log

 

早期文件日志收集采用过logstash,修改数据比Fluentd方便,日志收集,没有太多的修改过滤需求,可以切到更轻量的filebeat。

 

但缺点也很明显,这些日志只是容器的日志,日志本身并不含有容器的相关信息,一种实现应用时就把应用信息写到日志里,虽然没有容器信息,应用信息也足够识别。这些实际也引入项目开发的侵入性。

 

实际上,docker早期就有基本的容器信息,我们只要把这个容器信息,在收集时,附加到原始日志中即可。

 

很多人想到附加信息,会考虑从kube-api,或是从etcd读取,这样复杂度过高,基本都放弃了。

 

实际上,docker 包括contain 在内都可以附加 lable 信息,而这些都可直接从本地文件读取 

 

https://docs.docker.com/config/labels-custom-metadata/#key-format-recommendations

 

kuberetes 不支持为contain打label,但官方自已会加上一部分,这些lable基本够用了。

 

需求和简介描述完毕,以下是实现

 

看一眼config文件即知道实现方式,docker版本不同,文件名可能不一样,印象里早期版本是config.json,内容也没现在的全,早期版本不保证适用本文内容

 

cat /var/lib/docker/containers/caed8938198152778e3715f4bd3c00795f40c812d2cdb87dadfe8b0bb058390f/config.v2.json
 
caed8938198152778e3715f4bd3c00795f40c812d2cdb87dadfe8b0bb058390f是容器id,了解docker 应该知道 docker ps 第一列就是这个id的首几位,随便输一个本机的即可
{
    "StreamConfig": {},
    "State": {
        "Running": true,
        "Paused": false,
        "Restarting": false,
        "OOMKilled": false,
        "RemovalInProgress": false,
        "Dead": false,
        "Pid": 15201,
        "ExitCode": 0,
        "Error": "",
        "StartedAt": "2018-07-23T00:51:31.821704515Z",
        "FinishedAt": "2018-07-23T00:51:31.573484432Z",
        "Health": null
    },
    "ID": "caed8938198152778e3715f4bd3c00795f40c812d2cdb87dadfe8b0bb058390f",
    "Created": "2018-07-20T09:05:21.881001304Z",
    "Managed": false,
    "Path": "/app/app",
    "Args": [],
    "Config": {
        "Hostname": "consume-577cd986c7-8mk4h",
        "Domainname": "",
        "User": "0",
        "AttachStdin": false,
        "AttachStdout": false,
        "AttachStderr": false,
        "Tty": false,
        "OpenStdin": false,
        "StdinOnce": false,
        "Env": [
            "KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443",
            "TUICE_PERSONAL_WECHAT_PORT_8883_TCP_PORT=8883",
            "KUBERNETES_SERVICE_HOST=10.96.0.1",
            "KUBERNETES_PORT=tcp://10.96.0.1:443",
            "TUICE_PERSONAL_WECHAT_PORT=tcp://10.109.89.240:8883",
            "TUICE_PERSONAL_WECHAT_PORT_8883_TCP=tcp://10.109.89.240:8883",
            "KUBERNETES_SERVICE_PORT=443",
            "KUBERNETES_SERVICE_PORT_HTTPS=443",
            "KUBERNETES_PORT_443_TCP_PORT=443",
            "KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1",
            "TUICE_PERSONAL_WECHAT_PORT_8883_TCP_ADDR=10.109.89.240",
            "KUBERNETES_PORT_443_TCP_PROTO=tcp",
            "TUICE_PERSONAL_WECHAT_SERVICE_HOST=10.109.89.240",
            "TUICE_PERSONAL_WECHAT_SERVICE_PORT=8883",
            "TUICE_PERSONAL_WECHAT_SERVICE_PORT_SERVER=8883",
            "TUICE_PERSONAL_WECHAT_PORT_8883_TCP_PROTO=tcp",
            "PATH=/go/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
            "GOLANG_VERSION=1.8",
            "GOLANG_SRC_URL=https://golang.org/dl/go1.8.src.tar.gz",
            "GOLANG_SRC_SHA256=406865f587b44be7092f206d73fc1de252600b79b3cacc587b74b5ef5c623596",
            "GOPATH=/app",
            "RUN=pro"
        ],
        "Cmd": null,
        "Healthcheck": {
            "Test": [
                "NONE"
            ]
        },
        "Image": "hub.docker.admaster.co/social_base/consume@sha256:483cd48b4b1e2ca53846f11fe953695bcceea59542b77f09044d30644cf3235f",
        "Volumes": null,
        "WorkingDir": "/app",
        "Entrypoint": [
            "/app/app"
        ],
        "OnBuild": null,
        "Labels": {
            "annotation.io.kubernetes.container.hash": "e1a52ef6",
            "annotation.io.kubernetes.container.restartCount": "0",
            "annotation.io.kubernetes.container.terminationMessagePath": "/dev/termination-log",
            "annotation.io.kubernetes.container.terminationMessagePolicy": "File",
            "annotation.io.kubernetes.pod.terminationGracePeriod": "30",
            "io.kubernetes.container.logpath": "/var/log/pods/00bea8a1-8bfc-11e8-b709-f01fafd51338/consume/0.log",
            "io.kubernetes.container.name": "consume",
            "io.kubernetes.docker.type": "container",
            "io.kubernetes.pod.name": "consume-577cd986c7-8mk4h",
            "io.kubernetes.pod.namespace": "default",
            "io.kubernetes.pod.uid": "00bea8a1-8bfc-11e8-b709-f01fafd51338",
            "io.kubernetes.sandbox.id": "fda1d939f5b41e31ca5c5214b4d78b38691d438eeb52d22d4460d49a2a820c1f"
        }
    },
    "Image": "sha256:85c9ca987b6fd310ce1019c28031b670da4d6705e70f1807f17727d48aa4aef4",
    "NetworkSettings": {
        "Bridge": "",
        "SandboxID": "",
        "HairpinMode": false,
        "LinkLocalIPv6Address": "",
        "LinkLocalIPv6PrefixLen": 0,
        "Networks": null,
        "Service": null,
        "Ports": null,
        "SandboxKey": "",
        "SecondaryIPAddresses": null,
        "SecondaryIPv6Addresses": null,
        "IsAnonymousEndpoint": false,
        "HasSwarmEndpoint": false
    },
    "LogPath": "/var/lib/docker/containers/caed8938198152778e3715f4bd3c00795f40c812d2cdb87dadfe8b0bb058390f/caed8938198152778e3715f4bd3c00795f40c812d2cdb87dadfe8b0bb058390f-json.log",
    "Name": "/k8s_consume_consume-577cd986c7-8mk4h_default_00bea8a1-8bfc-11e8-b709-f01fafd51338_0",
    "Driver": "overlay",
    "MountLabel": "",
    "ProcessLabel": "",
    "RestartCount": 0,
    "HasBeenStartedBefore": true,
    "HasBeenManuallyStopped": false,
    "MountPoints": {
        "/app/conf": {
            "Source": "/var/lib/kubelet/pods/00bea8a1-8bfc-11e8-b709-f01fafd51338/volumes/kubernetes.io~configmap/conf",
            "Destination": "/app/conf",
            "RW": false,
            "Name": "",
            "Driver": "",
            "Type": "bind",
            "Relabel": "ro",
            "Propagation": "rprivate",
            "Spec": {
                "Type": "bind",
                "Source": "/var/lib/kubelet/pods/00bea8a1-8bfc-11e8-b709-f01fafd51338/volumes/kubernetes.io~configmap/conf",
                "Target": "/app/conf",
                "ReadOnly": true
            }
        },
        "/dev/termination-log": {
            "Source": "/var/lib/kubelet/pods/00bea8a1-8bfc-11e8-b709-f01fafd51338/containers/consume/fc3f9ebf",
            "Destination": "/dev/termination-log",
            "RW": true,
            "Name": "",
            "Driver": "",
            "Type": "bind",
            "Propagation": "rprivate",
            "Spec": {
                "Type": "bind",
                "Source": "/var/lib/kubelet/pods/00bea8a1-8bfc-11e8-b709-f01fafd51338/containers/consume/fc3f9ebf",
                "Target": "/dev/termination-log"
            }
        },
        "/etc/hosts": {
            "Source": "/var/lib/kubelet/pods/00bea8a1-8bfc-11e8-b709-f01fafd51338/etc-hosts",
            "Destination": "/etc/hosts",
            "RW": true,
            "Name": "",
            "Driver": "",
            "Type": "bind",
            "Propagation": "rprivate",
            "Spec": {
                "Type": "bind",
                "Source": "/var/lib/kubelet/pods/00bea8a1-8bfc-11e8-b709-f01fafd51338/etc-hosts",
                "Target": "/etc/hosts"
            }
        },
        "/var/log/contain": {
            "Source": "/var/lib/kubelet/pods/00bea8a1-8bfc-11e8-b709-f01fafd51338/volumes/kubernetes.io~local-volume/pv-3",
            "Destination": "/var/log/contain",
            "RW": true,
            "Name": "",
            "Driver": "",
            "Type": "bind",
            "Propagation": "rprivate",
            "Spec": {
                "Type": "bind",
                "Source": "/var/lib/kubelet/pods/00bea8a1-8bfc-11e8-b709-f01fafd51338/volumes/kubernetes.io~local-volume/pv-3",
                "Target": "/var/log/contain"
            }
        },
        "/tmp/nbbsdownload": {
            "Source": "/var/lib/kubelet/pods/00bea8a1-8bfc-11e8-b709-f01fafd51338/volumes/kubernetes.io~local-volume/pv-3-6",
            "Destination": "/tmp/nbbsdownload",
            "RW": true,
            "Name": "",
            "Driver": "",
            "Type": "bind",
            "Propagation": "rprivate",
            "Spec": {
                "Type": "bind",
                "Source": "/var/lib/kubelet/pods/00bea8a1-8bfc-11e8-b709-f01fafd51338/volumes/kubernetes.io~local-volume/pv-3-6",
                "Target": "/tmp/nbbsdownload"
            }
        },
        "/var/run/secrets/kubernetes.io/serviceaccount": {
            "Source": "/var/lib/kubelet/pods/00bea8a1-8bfc-11e8-b709-f01fafd51338/volumes/kubernetes.io~secret/default-token-g6gr9",
            "Destination": "/var/run/secrets/kubernetes.io/serviceaccount",
            "RW": false,
            "Name": "",
            "Driver": "",
            "Type": "bind",
            "Relabel": "ro",
            "Propagation": "rprivate",
            "Spec": {
                "Type": "bind",
                "Source": "/var/lib/kubelet/pods/00bea8a1-8bfc-11e8-b709-f01fafd51338/volumes/kubernetes.io~secret/default-token-g6gr9",
                "Target": "/var/run/secrets/kubernetes.io/serviceaccount",
                "ReadOnly": true
            }
        }
    },
    "SecretReferences": null,
    "AppArmorProfile": "",
    "HostnamePath": "/var/lib/docker/containers/fda1d939f5b41e31ca5c5214b4d78b38691d438eeb52d22d4460d49a2a820c1f/hostname",
    "HostsPath": "/var/lib/kubelet/pods/00bea8a1-8bfc-11e8-b709-f01fafd51338/etc-hosts",
    "ShmPath": "/var/lib/docker/containers/fda1d939f5b41e31ca5c5214b4d78b38691d438eeb52d22d4460d49a2a820c1f/shm",
    "ResolvConfPath": "/var/lib/docker/containers/fda1d939f5b41e31ca5c5214b4d78b38691d438eeb52d22d4460d49a2a820c1f/resolv.conf",
    "SeccompProfile": "unconfined",
    "NoNewPrivileges": false
}
View Code

 

如果是k8s启动的contain,会有k8s相应的lable,应用这部分数据

{
            "annotation.io.kubernetes.container.hash": "e1a52ef6",
            "annotation.io.kubernetes.container.restartCount": "0",
            "annotation.io.kubernetes.container.terminationMessagePath": "/dev/termination-log",
            "annotation.io.kubernetes.container.terminationMessagePolicy": "File",
            "annotation.io.kubernetes.pod.terminationGracePeriod": "30",
            "io.kubernetes.container.logpath": "/var/log/pods/00bea8a1-8bfc-11e8-b709-f01fafd51338/consume/0.log",
            "io.kubernetes.container.name": "consume",
            "io.kubernetes.docker.type": "container",
            "io.kubernetes.pod.name": "consume-577cd986c7-8mk4h",
            "io.kubernetes.pod.namespace": "default",
            "io.kubernetes.pod.uid": "00bea8a1-8bfc-11e8-b709-f01fafd51338",
            "io.kubernetes.sandbox.id": "fda1d939f5b41e31ca5c5214b4d78babcd1d438eeb52d22d4460d49a2a820c1f"
}

docker的原始日志json file 文件 是

/var/lib/docker/containers/caed8938198152778e3715f4bd3c00795f40c812d2cdb87dadfe8b0bb058390f/caed8938198152778e3715f4bd3c00795f40c812d2cdb87dadfe8b0bb058390f-json.log

 

收集这个文件,再把label里的相应数据加上,就是k8s的容器日志了,说穿了很简单,只是很多人不清楚这几个文件的存在罢了。

 

这里收集的是docker out的日志,另一种普遍的作法是,映射一个本地路径,把文件写在这个目录下。

在容器内执行,收集容器内路径下的日志,或在宿主机执行,收集容器外路径下的日志。

 

这里最好能和docker out的收集方式统一,用两套完全不可接受

 

实际实现也很简单,以上的信息里还有这一部分

{
        "/var/log/contain": {
            "Source": "/var/lib/kubelet/pods/00bea8a1-8bfc-11e8-b709-f01fafd51338/volumes/kubernetes.io~local-volume/pv-3",
            "Destination": "/var/log/contain",
            "RW": true,
            "Name": "",
            "Driver": "",
            "Type": "bind",
            "Propagation": "rprivate",
            "Spec": {
                "Type": "bind",
                "Source": "/var/lib/kubelet/pods/00bea8a1-8bfc-11e8-b709-f01fafd51338/volumes/kubernetes.io~local-volume/pv-3",
                "Target": "/var/log/contain"
            }
        }
}

这里mount的映射信息,

/var/log/contain是容器内路径,
/var/lib/kubelet/pods/00bea8a1-8bfc-11e8-b709-f01fafd51338/volumes/kubernetes.io~local-volume/pv-3是宿主机路径

把这个

/var/lib/kubelet/pods/00bea8a1-8bfc-11e8-b709-f01fafd51338/volumes/kubernetes.io~local-volume/pv-3目录也加到收集监控里便ok了

 

思路已经通了

最后我们要的是类似这样的一个配置文件(不论是不是filebeat,需要的数据就是这些)

 

补充一点,看下方输出就明白

 ls -alh /var/log/pods/00bea8a1-8bfc-11e8-b709-f01fafd51338/consume/0.log
lrwxrwxrwx 1 root root 165 Jul 20 17:05 /var/log/pods/00bea8a1-8bfc-11e8-b709-f01fafd51338/consume/0.log -> /var/lib/docker/containers/caed8938198152778e3715f4bd3c00795f40c812d2cdb87dadfe8b0bb058390f/caed8938198152778e3715f4bd3c00795f40c812d2cdb87dadfe8b0bb058390f-json.log
 
filebeat.inputs:
- type: log
  paths:
    - /var/lib/docker/containers/caed8938198152778e3715f4bd3c00795f40c812d2cdb87dadfe8b0bb058390f/caed8938198152778e3715f4bd3c00795f40c812d2cdb87dadfe8b0bb058390f-json.log
    - /var/lib/kubelet/pods/00bea8a1-8bfc-11e8-b709-f01fafd51338/volumes/kubernetes.io~local-volume/pv-3/*.log
  fields:
    namespace: "default"
    name: "consume"
    pod_name: "consume-577cd986c7-8mk4h"
output.elasticsearch:
  hosts: ["localhost:9200"]
setup.kibana:
  host: "localhost:5601"

 

定时扫描/var/lib/docker/containers/,读取每个contain的config.v2.json,构造配置文件,启动收集进程

 

因为contain id包含hash值,同名冲突的概率几乎0零,因此也不用考虑任何冲突的问题,每个k8s node独立部署一个脚本即可。

 

目前实现的方式是用脚本加定时器,实时性稍微差了些,够用了,有工夫的可以优化成文件监听。


推荐阅读
  • 本文深入探讨了ASP.NET Web API与RESTful架构的设计与实现。ASP.NET Web API 是一个强大的框架,能够简化HTTP服务的开发,使其能够广泛支持各种客户端设备。通过详细分析其核心原理和最佳实践,本文为开发者提供了构建高效、可扩展且易于维护的Web服务的指导。此外,还讨论了如何利用RESTful原则优化API设计,确保系统的灵活性和互操作性。 ... [详细]
  • 通过将常用的外部命令集成到VSCode中,可以提高开发效率。本文介绍如何在VSCode中配置和使用自定义的外部命令,从而简化命令执行过程。 ... [详细]
  • 在本文中,我们将探讨如何在Docker环境中高效地管理和利用数据库。首先,需要安装Docker Desktop以确保本地环境准备就绪。接下来,可以从Docker Hub中选择合适的数据库镜像,并通过简单的命令将其拉取到本地。此外,我们还将介绍如何配置和优化这些数据库容器,以实现最佳性能和安全性。 ... [详细]
  • 在ElasticStack日志监控系统中,Logstash编码插件自5.0版本起进行了重大改进。插件被独立拆分为gem包,每个插件可以单独进行更新和维护,无需依赖Logstash的整体升级。这不仅提高了系统的灵活性和可维护性,还简化了插件的管理和部署过程。本文将详细介绍这些编码插件的功能、配置方法,并通过实际生产环境中的应用案例,展示其在日志处理和监控中的高效性和可靠性。 ... [详细]
  • Python 伦理黑客技术:深入探讨后门攻击(第三部分)
    在《Python 伦理黑客技术:深入探讨后门攻击(第三部分)》中,作者详细分析了后门攻击中的Socket问题。由于TCP协议基于流,难以确定消息批次的结束点,这给后门攻击的实现带来了挑战。为了解决这一问题,文章提出了一系列有效的技术方案,包括使用特定的分隔符和长度前缀,以确保数据包的准确传输和解析。这些方法不仅提高了攻击的隐蔽性和可靠性,还为安全研究人员提供了宝贵的参考。 ... [详细]
  • Java解析YAML文件并转换为JSON格式(支持JSON与XML的结构化查询)
    本文探讨了如何利用Java解析YAML文件并将其转换为JSON格式,同时支持JSON和XML的结构化查询。YAML、JSON和XML这三种数据格式通过其名称作为文件扩展名,便于区分和使用。文章详细介绍了这些格式的层次结构和数据表示方法,并重点讨论了在数据传输过程中,XML的特性和优势。此外,还提供了具体的代码示例和实现步骤,帮助开发者高效地进行数据格式转换和查询操作。 ... [详细]
  • Elasticsearch 嵌套调用中动态类导致数据返回异常分析与解决方案 ... [详细]
  • 掌握 esrally 三步骤:高效执行 Elasticsearch 性能测试任务
    自从上次发布 esrally 教程已近两个月,期间不断有用户咨询使用过程中遇到的各种问题,尤其是由于测试数据托管在海外 AWS 上,导致下载速度极慢。为此,本文将详细介绍如何通过三个关键步骤高效执行 Elasticsearch 性能测试任务,帮助用户解决常见问题并提升测试效率。 ... [详细]
  • 浏览器作为我们日常不可或缺的软件工具,其背后的运作机制却鲜为人知。本文将深入探讨浏览器内核及其版本的演变历程,帮助读者更好地理解这一关键技术组件,揭示其内部运作的奥秘。 ... [详细]
  • 基于Net Core 3.0与Web API的前后端分离开发:Vue.js在前端的应用
    本文介绍了如何使用Net Core 3.0和Web API进行前后端分离开发,并重点探讨了Vue.js在前端的应用。后端采用MySQL数据库和EF Core框架进行数据操作,开发环境为Windows 10和Visual Studio 2019,MySQL服务器版本为8.0.16。文章详细描述了API项目的创建过程、启动步骤以及必要的插件安装,为开发者提供了一套完整的开发指南。 ... [详细]
  • 深入探索HTTP协议的学习与实践
    在初次访问某个网站时,由于本地没有缓存,服务器会返回一个200状态码的响应,并在响应头中设置Etag和Last-Modified等缓存控制字段。这些字段用于后续请求时验证资源是否已更新,从而提高页面加载速度和减少带宽消耗。本文将深入探讨HTTP缓存机制及其在实际应用中的优化策略,帮助读者更好地理解和运用HTTP协议。 ... [详细]
  • 在安装并配置了Elasticsearch后,我在尝试通过GET /_nodes请求获取节点信息时遇到了问题,收到了错误消息。为了确保请求的正确性和安全性,我需要进一步排查配置和网络设置,以确保Elasticsearch集群能够正常响应。此外,还需要检查安全设置,如防火墙规则和认证机制,以防止未经授权的访问。 ... [详细]
  • 人人租机作为国内领先的信用免押租赁平台,为企业和个人提供全方位的新租赁服务。通过接入支付宝小程序功能,该平台实现了从零到百的迅猛增长,成为全国首家推出“新租赁小程序”开发服务的阿里巴巴小程序服务商(ISV)。这一创新举措不仅提升了用户体验,还显著增强了平台的市场竞争力。 ... [详细]
  • 利用Jenkins与SonarQube集成实现高效代码质量检测与优化
    本文探讨了通过在 Jenkins 多分支流水线中集成 SonarQube,实现高效且自动化的代码质量检测与优化方法。该方案不仅提高了开发团队的代码审查效率,还确保了软件项目的持续高质量交付。 ... [详细]
  • grafana,9,1,1,发布,系统,指标, ... [详细]
author-avatar
星魂陨石
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有