Kubernetes日志采集——使用Fluent Bit采集和转发Kubernetes日志(三)
1、概览
本文主要讲解下如何编写Fluent Bit配置文件来采集和转发Kubernetes日志。如果对Kubernetes日志管理机制和Fluent Bit不熟悉,请先阅读《从 Docker 到 Kubernetes 日志管理机制详解》、《Kubernetes日志采集——Fluent Bit详细介绍(一)》、《Kubernetes日志采集——Fluent Bit插件详细配置(二)》这三篇博文。
2、Kubernetes 的日志种类
在 Kubernetes 中日志也主要有两大类:
- Kuberntes 集群组件日志;
- 应用 Pod 日志;
所以,使用Fluent Bit采集Kubernetes日志就是采集Kuberntes 集群组件日志和应用 Pod 日志。
2.1 Kuberntes 集群组件日志
Kuberntes 集群组件日志分为两类:
- 运行在容器中的 Kubernetes scheduler 和 kube-proxy等。
- 未运行在容器中的 kubelet 和容器 runtime,比如 Docker。
在使用 systemd 机制的服务器上,kubelet 和容器 runtime 写入日志到 journald(常用的centos7正是使用 systemd 机制)。如果没有 systemd,他们写入日志到 /var/log 目录的 .log 文件。
2.2 应用 Pod 日志
Kubernetes Pod 的日志管理是基于 Docker 引擎的,Kubernetes 并不管理日志的轮转策略,日志的存储都是基于 Docker 的日志管理策略。k8s 集群调度的基本单位就是 Pod,而 Pod 是一组容器,所以 k8s 日志管理基于 Docker 引擎这一说法也就不难理解了,最终日志还是要落到一个个容器上面。
3、在Kubernetes集群部署Fluentbit
由于在Kubernetes部署Fluent-bit Daemonset比较简单,本文就不再介绍Fluent-bit Daemonset的安装过程。
下面粘贴一下Fluentbit Daemonset的配置文件,对于Fluentbit Daemonset的配置文件着重看下volumeMounts部分。
- 把节点的/var/log/journal目录挂载到fluent-bit容器内,通过/var/log/journal目录即可采集Kuberntes 集群组件日志。
- 把节点的/var/log目录和/data/docker-data/containers(docker数据盘路径)目录挂载到fluent-bit容器内,通过/var/log/containers/目录即可采集当前节点所有Pod下所有容器的所有日志文件(这里如果有疑问可以参见《从 Docker 到 Kubernetes 日志管理机制详解》)。
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 73 74 75 76 77 78 79 80 81 82 83 84 85 | apiVersion: apps/v1 kind: DaemonSet metadata: labels: app.kubernetes.io/name: fluent-bit name: fluent-bit namespace: logging-system spec: revisionHistoryLimit: 10 selector: matchLabels: app.kubernetes.io/name: fluent-bit template: metadata: creationTimestamp: null labels: app.kubernetes.io/name: fluent-bit name: fluent-bit namespace: logging-system spec: containers: - env: - name: NODE_NAME valueFrom: fieldRef: apiVersion: v1 fieldPath: spec.nodeName image: fluent-bit:v1.8.3 imagePullPolicy: IfNotPresent name: fluent-bit ports: - containerPort: 2020 name: metrics protocol: TCP resources: {} terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: - mountPath: /fluent-bit/config name: config readOnly: true - mountPath: /var/log/ name: varlogs readOnly: true - mountPath: /var/log/journal name: systemd readOnly: true - mountPath: /fluent-bit/tail name: positions - mountPath: /data/docker-data/containers name: varlibcontainers0 readOnly: true dnsPolicy: ClusterFirst restartPolicy: Always schedulerName: default-scheduler securityContext: {} serviceAccount: fluent-bit serviceAccountName: fluent-bit terminationGracePeriodSeconds: 30 tolerations: - operator: Exists volumes: - name: config secret: defaultMode: 420 secretName: fluent-bit-config - hostPath: path: /var/log type: "" name: varlogs - hostPath: path: /var/log/journal type: "" name: systemd - emptyDir: {} name: positions - hostPath: path: /data/docker-data/containers type: "" name: varlibcontainers0 updateStrategy: rollingUpdate: maxSurge: 0 maxUnavailable: 1 type: RollingUpdate |
注意:配置文件中的fluent-bit:v1.8.3镜像是基于官方镜像二次构建的,所以不要直接粘贴以上Fluentbit Daemonset的配置文件到其他k8s环境部署。另外,fluent-bit采集和转发的配置是通过密钥的形式挂载到fluent-bit容器内部,由于二次构建了镜像所以fluent-bit启动时能直接加载此密钥里面的配置文件。
4、通过编写Fluent Bit配置文件来采集和转发Kubernetes日志
此章节是本文的核心,通过配置Fluent Bit输入、解析、过滤、缓存和输出模块来采集和转发Kubernetes日志。
这里先整体粘贴下Fluent Bit输入、解析、过滤、缓存和输出模块的配置,下文会依次解释Input、Parser、Filter、Buffer、Routing 和 Output模块的配置。
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 | [Service] Parsers_File parsers.conf [Input] Name systemd Path /var/log/journal DB /fluent-bit/tail/docker.db DB.Sync Normal Tag service.docker Systemd_Filter _SYSTEMD_UNIT=docker.service [Input] Name systemd Path /var/log/journal DB /fluent-bit/tail/kubelet.db DB.Sync Normal Tag service.kubelet Systemd_Filter _SYSTEMD_UNIT=kubelet.service [Input] Name tail Path /var/log/containers/*.log Exclude_Path /var/log/containers/*_cloudbases-logging-system_events-exporter*.log,/var/log/containers/kube-auditing-webhook*_cloudbases-logging-system_kube-auditing-webhook*.log Refresh_Interval 10 Skip_Long_Lines true DB /fluent-bit/tail/pos.db DB.Sync Normal Mem_Buf_Limit 5MB Parser docker Tag kube.* [Filter] Name kubernetes Match kube.* Kube_URL https://kubernetes.default.svc:443 Kube_CA_File /var/run/secrets/kubernetes.io/serviceaccount/ca.crt Kube_Token_File /var/run/secrets/kubernetes.io/serviceaccount/token Labels false Annotations false [Filter] Name nest Match kube.* Operation lift Nested_under kubernetes Add_prefix kubernetes_ [Filter] Name modify Match kube.* Remove stream Remove kubernetes_pod_id Remove kubernetes_host Remove kubernetes_container_hash [Filter] Name nest Match kube.* Operation nest Wildcard kubernetes_* Nest_under kubernetes Remove_prefix kubernetes_ [Filter] Name lua Match service.* script /fluent-bit/config/systemd.lua call add_time time_as_table true [Output] Name es Match_Regex (?:kube|service)\.(.*) Host elasticsearch-logging-data.logging-system.svc Port 9200 Logstash_Format true Logstash_Prefix cb-logstash-log Time_Key @timestamp Generate_ID true |
注意:docker容器日志默认都是以JSON 的格式写到文件中,每一条 json 日志中默认包含 log
, stream
, time
三个字段。
1 2 3 4 5 | { "log": ...., "stream": ....., "time": ....... } |
以下图这条容器日志为例,下面会详细说明此条日志在Fluent Bit不同模块的日志格式:
4.1 全局配置——SERVICE
1 2 | [Service] Parsers_File parsers.conf |
这里Parsers_File引用了parsers.conf配置文件,在[Input]模块会使用parsers.conf文件中定义的[PARSER]将 Input 抽取的非结构化数据转化为标准的结构化数据,下面粘贴一下parsers.conf配置文件内容:
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 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | [PARSER] Name apache Format regex Regex ^(?< host >[^ ]*) [^ ]* (?< user >[^ ]*) \[(?< time >[^\]]*)\] "(?< method >\S+)(?: +(?< path >[^\"]*?)(?: +\S*)?)?" (?< code >[^ ]*) (?< size >[^ ]*)(?: "(?< referer >[^\"]*)" "(?< agent >[^\"]*)")?$ Time_Key time Time_Format %d/%b/%Y:%H:%M:%S %z [PARSER] Name apache2 Format regex Regex ^(?< host >[^ ]*) [^ ]* (?< user >[^ ]*) \[(?< time >[^\]]*)\] "(?< method >\S+)(?: +(?< path >[^ ]*) +\S*)?" (?< code >[^ ]*) (?< size >[^ ]*)(?: "(?< referer >[^\"]*)" "(?< agent >.*)")?$ Time_Key time Time_Format %d/%b/%Y:%H:%M:%S %z [PARSER] Name apache_error Format regex Regex ^\[[^ ]* (?< time >[^\]]*)\] \[(?< level >[^\]]*)\](?: \[pid (?< pid >[^\]]*)\])?( \[client (?< client >[^\]]*)\])? (?< message >.*)$ [PARSER] Name nginx Format regex Regex ^(?< remote >[^ ]*) (?< host >[^ ]*) (?< user >[^ ]*) \[(?< time >[^\]]*)\] "(?< method >\S+)(?: +(?< path >[^\"]*?)(?: +\S*)?)?" (?< code >[^ ]*) (?< size >[^ ]*)(?: "(?< referer >[^\"]*)" "(?< agent >[^\"]*)") Time_Key time Time_Format %d/%b/%Y:%H:%M:%S %z [PARSER] # https://rubular.com/r/IhIbCAIs7ImOkc Name k8s-nginx-ingress Format regex Regex ^(?< host >[^ ]*) - (?< user >[^ ]*) \[(?< time >[^\]]*)\] "(?< method >\S+)(?: +(?< path >[^\"]*?)(?: +\S*)?)?" (?< code >[^ ]*) (?< size >[^ ]*) "(?< referer >[^\"]*)" "(?< agent >[^\"]*)" (?< request_length >[^ ]*) (?< request_time >[^ ]*) \[(?< proxy_upstream_name >[^ ]*)\] (\[(?< proxy_alternative_upstream_name >[^ ]*)\] )?(?< upstream_addr >[^ ]*) (?< upstream_response_length >[^ ]*) (?< upstream_response_time >[^ ]*) (?< upstream_status >[^ ]*) (?< reg_id >[^ ]*).*$ Time_Key time Time_Format %d/%b/%Y:%H:%M:%S %z [PARSER] Name json Format json Time_Key time Time_Format %d/%b/%Y:%H:%M:%S %z [PARSER] Name docker Format json Time_Key time Time_Format %Y-%m-%dT%H:%M:%S.%L Time_Keep On # -- # Since Fluent Bit v1.2, if you are parsing Docker logs and using # the Kubernetes filter, it's not longer required to decode the # 'log' key. # # Command | Decoder | Field | Optional Action # =============|==================|================= #Decode_Field_As json log [PARSER] Name docker-daemon Format regex Regex time="(?< time >[^ ]*)" level=(?< level >[^ ]*) msg="(?< msg >[^ ].*)" Time_Key time Time_Format %Y-%m-%dT%H:%M:%S.%L Time_Keep On [PARSER] Name syslog-rfc5424 Format regex Regex ^\<(?< pri >[0-9]{1,5})\>1 (?< time >[^ ]+) (?< host >[^ ]+) (?< ident >[^ ]+) (?< pid >[-0-9]+) (?< msgid >[^ ]+) (?< extradata >(\[(.*?)\]|-)) (?< message >.+)$ Time_Key time Time_Format %Y-%m-%dT%H:%M:%S.%L%z Time_Keep On [PARSER] Name syslog-rfc3164-local Format regex Regex ^\<(?< pri >[0-9]+)\>(?< time >[^ ]* {1,2}[^ ]* [^ ]*) (?< ident >[a-zA-Z0-9_\/\.\-]*)(?:\[(?< pid >[0-9]+)\])?(?:[^\:]*\:)? *(?< message >.*)$ Time_Key time Time_Format %b %d %H:%M:%S Time_Keep On [PARSER] Name syslog-rfc3164 Format regex Regex /^\<(?< pri >[0-9]+)\>(?< time >[^ ]* {1,2}[^ ]* [^ ]*) (?< host >[^ ]*) (?< ident >[a-zA-Z0-9_\/\.\-]*)(?:\[(?< pid >[0-9]+)\])?(?:[^\:]*\:)? *(?< message >.*)$/ Time_Key time Time_Format %b %d %H:%M:%S Time_Keep On [PARSER] Name mongodb Format regex Regex ^(?< time >[^ ]*)\s+(?< severity >\w)\s+(?< component >[^ ]+)\s+\[(?< context >[^\]]+)]\s+(?< message >.*?) *(?< ms >(\d+))?(:?ms)?$ Time_Format %Y-%m-%dT%H:%M:%S.%L Time_Keep On Time_Key time [PARSER] # https://rubular.com/r/3fVxCrE5iFiZim Name envoy Format regex Regex ^\[(?< start_time >[^\]]*)\] "(?< method >\S+)(?: +(?< path >[^\"]*?)(?: +\S*)?)? (?< protocol >\S+)" (?< code >[^ ]*) (?< response_flags >[^ ]*) (?< bytes_received >[^ ]*) (?< bytes_sent >[^ ]*) (?< duration >[^ ]*) (?< x_envoy_upstream_service_time >[^ ]*) "(?< x_forwarded_for >[^ ]*)" "(?< user_agent >[^\"]*)" "(?< request_id >[^\"]*)" "(?< authority >[^ ]*)" "(?< upstream_host >[^ ]*)" Time_Format %Y-%m-%dT%H:%M:%S.%L%z Time_Keep On Time_Key start_time [PARSER] # http://rubular.com/r/tjUt3Awgg4 Name cri Format regex Regex ^(?< time >[^ ]+) (?< stream >stdout|stderr) (?< logtag >[^ ]*) (?< message >.*)$ Time_Key time Time_Format %Y-%m-%dT%H:%M:%S.%L%z [PARSER] Name kube-custom Format regex Regex (?< tag >[^.]+)?\.?(?< pod_name >[a-z0-9](?:[-a-z0-9]*[a-z0-9])?(?:\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)_(?< namespace_name >[^_]+)_(?< container_name >.+)-(?< docker_id >[a-z0-9]{64})\.log$ |
4.2 输入——Input(包含解析模块引用)
通过配置[Input]模块采集Kubernetes集群组件日志和应用容器日志。
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 | [Input] Name systemd //使用systemd输入插件从systemd或journaled读取日志 Path /var/log/journal //采集k8s未运行在容器中的集群组件日志: Docker DB /fluent-bit/tail/docker.db DB.Sync Normal Tag service.docker //定义Tag用于识别数据源 Systemd_Filter _SYSTEMD_UNIT=docker.service //采集当前节点docker服务日志(_SYSTEM_UNIT必须加下划线) [Input] Name systemd Path /var/log/journal //采集k8s未运行在容器中的集群组件日志: Kubelet DB /fluent-bit/tail/kubelet.db DB.Sync Normal Tag service.kubelet Systemd_Filter _SYSTEMD_UNIT=kubelet.service //采集当前节点kubelet服务日志 [Input] Name tail //使用tail输入插件 Path /var/log/containers/*.log //采集k8s应用Pod日志和运行在容器中的集群组件日志(Kubernetes scheduler 和 kube-proxy、etcd等) Exclude_Path /var/log/containers/*_cloudbases-logging-system_events-exporter*.log,/var/log/containers/kube-auditing-webhook*_cloudbases-logging-system_kube-auditing-webhook*.log //使用通配符排除日志文件采集 Refresh_Interval 10 //刷新监视文件列表的时间间隔 Skip_Long_Lines true //当受监视的文件由于行很长而达到缓冲区容量时,默认停止监视该文件 DB /fluent-bit/tail/pos.db DB.Sync Normal Mem_Buf_Limit 5MB //缓存使用的内存限制如果达到了极限,input就会暂停读取;当刷新数据到output后,它将恢复读取 Parser docker //使用docker解析插件将Input抽取的非结构化容器日志转化为标准的结构化数据 Tag kube.* |
应用容器日志通过配置[Input]模块中引用docker解析插件将Input抽取的非结构化容器日志转化为标准的结构化数据,此时容器的日志格式为:
docker解析插件内容如下:
1 2 3 4 5 6 | [PARSER] Name docker Format json Time_Key time Time_Format %Y-%m-%dT%H:%M:%S.%L Time_Keep On |
4.3 过滤——Filter
对Input模块采集的格式化数据进行过滤和修改。一个数据管道中可以包含多个 Filter,Filter 会顺序执行,其执行顺序与配置文件中的顺序一致。
4.3.1 使用kubernetes过滤器插件为应用容器日志和运行在容器中的k8s集群组件日志添加kubernetes元数据
1 2 3 4 5 6 7 8 | [Filter] Name kubernetes Match kube.* //匹配输入模块中的Tag,即匹配上文中的使用tail插件的那个input模块 Kube_URL https://kubernetes.default.svc:443 Kube_CA_File /var/run/secrets/kubernetes.io/serviceaccount/ca.crt Kube_Token_File /var/run/secrets/kubernetes.io/serviceaccount/token Labels false //不将标签添加到容器日志中 Annotations false //不将注解添加到容器日志中 |
经过kubernetes过滤器插件后,此时容器的日志格式为:
4.3.2 使用nest过滤器插件对应用容器和运行在容器中的k8s集群组件的嵌套日志进行操作
1 2 3 4 5 6 | [Filter] Name nest Match kube.* Operation lift //通过lift模式,从记录的将指定map中的key value都提取出来放到上一层 Nested_under kubernetes //指定需要提取的map名 Add_prefix kubernetes_ //添加前缀 |
经过nest过滤器插件后,此时容器的日志格式为:
4.3.3 通过modify调整应用容器和运行在容器中的k8s集群组件日志字段
1 2 3 4 5 6 7 | [Filter] Name modify Match kube.* Remove stream //移除stream字段 Remove kubernetes_pod_id //移除kubernetes_pod_id字段 Remove kubernetes_host //移除kubernetes_host字段 Remove kubernetes_container_hash //移除kubernetes_container_hash字段 |

4.3.4 使用nest过滤器插件对应用容器和运行在容器中的k8s集群组件的嵌套日志进行操作
1 2 3 4 5 6 7 | [Filter] Name nest Match kube.* Operation nest //通过nest模式,从记录中指定一组key value合并,并放到一个map里 Wildcard kubernetes_* //选择日志记录中以kubernetes_为前缀的key,将这些key value放到一个map里 Nest_under kubernetes //存放key value的map名 Remove_prefix kubernetes_ //移除这些key的前缀 |
经过nest过滤器插件后,此时容器的日志格式为:
经过以上4个过滤器插件后,将应用容器日志和运行在容器中的k8s集群组件日志过滤和修改成了想要的格式,当以上配置不满足公司业务需求时,对应调整过滤器模块配置即可。
4.3.5 通过lua过滤器插件处理kubernetes非容器化集群组件(Docker、Kubelet)日志
需要注意的是经过systemd输入插件采集的日志直接是格式化的,并不需要解析。
示例日志如下:
1 | service.kubelet: [1657004329.109221000, {"PRIORITY"=>"6", "_UID"=>"0", "_GID"=>"0", "_CAP_EFFECTIVE"=>"1fffffffff", "_SYSTEMD_SLICE"=>"system.slice", "_BOOT_ID"=>"f1b154f127cf479f9c150f84038fd70b", "_MACHINE_ID"=>"d96b070ae8844338a3170e4ee73453f8", "_HOSTNAME"=>"node1", "_TRANSPORT"=>"stdout", "_STREAM_ID"=>"f984c86546344c88811be43d44b93394", "SYSLOG_FACILITY"=>"3", "SYSLOG_IDENTIFIER"=>"kubelet", "_PID"=>"79465", "_COMM"=>"kubelet", "_EXE"=>"/usr/local/bin/kubelet", "_CMDLINE"=>"/usr/local/bin/kubelet --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf --config=/var/lib/kubelet/config.yaml --cgroup-driver=cgroupfs --network-plugin=cni --pod-infra-container-image=harbor.openserver.cn:443/big_data-cloudbases/pause:3.4.1 --node-ip=10.20.30.31 --hostname-override=node1", "_SYSTEMD_CGROUP"=>"/system.slice/kubelet.service", "_SYSTEMD_UNIT"=>"kubelet.service", "MESSAGE"=>"E0705 14:58:49.108605 79465 cadvisor_stats_provider.go:151] "Unable to fetch pod etc hosts stats" err="failed to get stats failed command 'du' ($ nice -n 19 du -x -s -B 1) on path /var/lib/kubelet/pods/57d16b3d-23f5-4a40-87a6-9e547793a519/etc-hosts with error exit status 1" pod="ingress-nginx/ingress-nginx-admission-create-2q7xc""}] |
接着看下kubernetes非容器化集群组件的过滤器配置:
1 2 3 4 5 6 | [Filter] Name lua Match service.* script /fluent-bit/config/systemd.lua //脚本文件 call add_time //调用脚本add_time方法 time_as_table true |
其中script脚本内容如下,逻辑为:新生成个空的日志记录,然后将采集到的systemd服务日志记录的指定字段放到新的日志记录中,然后返回新组装的日志记录,将新组装的日志记录输出到指定目的地。这样就可以将采集到的systemd服务日志过滤和修改成我们想要的日志内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | function add_time(tag, timestamp, record) //record是获取的日志记录 new_record = {} //初始化一个空的日志记录 //时间格式化 timeStr = os.date("!*t", timestamp["sec"]) t = string.format("%4d-%02d-%02dT%02d:%02d:%02d.%sZ", timeStr["year"], timeStr["month"], timeStr["day"], timeStr["hour"], timeStr["min"], timeStr["sec"], timestamp["nsec"]) //初始化空的kubernetes map 并新增数据 kubernetes = {} kubernetes["pod_name"] = record["_HOSTNAME"] kubernetes["container_name"] = record["SYSLOG_IDENTIFIER"] kubernetes["namespace_name"] = "kube-system" //把新增的数据都放到空的map中 new_record["time"] = t new_record["log"] = record["MESSAGE"] new_record["kubernetes"] = kubernetes return 1, timestamp, new_record |
4.4 输出——Output
将数据发送到不同的目的地。
1 2 3 4 5 6 7 8 9 | [Output] Name es //输出插件使用es Match_Regex (?:kube|service)\.(.*) //在输出模块配置中指定 Match 规则,Match输入模块中的Tag,这样通过标签和匹配规则就能将数据路由到一个或多个目的地 Host elasticsearch-logging-data.logging-system.svc //es地址 Port 9200 //es端口 Logstash_Format true Logstash_Prefix cb-logstash-log Time_Key @timestamp Generate_ID true |
5、总结
Fluent Bit采集Kubernetes日志就是采集Kuberntes 集群组件日志和应用 Pod 日志,其中Kuberneters集群组件日志又分为:
- 运行在容器中的 Kubernetes scheduler 和 kube-proxy等。
- 未运行在容器中的 kubelet 和容器 runtime,比如 Docker。
通过分析Kubernetes日志种类就能明确出日志采集点,对于容器日志采集/var/log/containers/路径即可,对于非容器服务根据服务名通过systemd插件采集即可,至此就可以将Kubernetes日志采集到。接下来再通过编写Fluent Bit输入、解析、过滤、缓存和输出模块的配置就可以将Kubernetes日志转换成我们想要的格式,并将日志输出到指定目的地。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
2021-07-08 优化Kubernetes集群DNS性能
2021-07-08 (转)Kubernetes内部域名解析原理、弊端及优化方式