为了账号安全,请及时绑定邮箱和手机立即绑定

当事情出错时:OpenTelemetry Operator的故障排查指南

与丽思李共著

A storm drain on an asphalt road is partially covered with fallen autumn leaves. The leaves are in various shades of red, orange, and brown, indicating the fall season. The drain has a distinctive V-shaped pattern with parallel slits for water to pass through. Some small yellow seed pods are also scattered among the leaves.

Adriana Villela 拍摄的照片:秋叶堆在排水沟旁。

如果你已经有一个应用在 Kubernetes 上运行,并且正在探索使用 OpenTelemetry 来了解你应用和集群的健康和性能的洞察,你可能对名为 [Kubernetes操作符] 的 [OpenTelemetry操作符] 感兴趣。

你很快就会发现,由于它广泛的能力,Operator 是你管理 OpenTelemetry 的首选工具(几乎无烦恼)。但是,和任何强大的工具一样,当出现问题时又会如何呢?

在这篇博客里,你将了解 OpenTelemetry Operator(以下简称“Operator”),以及在安装、Collector 部署和自动仪器化过程中常见的问题和挑战。你还将学习如何解决这些问题,从而更好地准备运行 Operator,使自己更加有准备。

操作员简介

让我们更详细地了解运营者的主要能力。

管理采集器:

操作员会自动部署您的收集器,并确保它在集群中正确配置并顺畅运行。操作员还使用Open Agent Management Protocol(OpAMP)跨多个收集器管理配置,这是一种用于远程管理大量数据收集代理的网络协议,因此有助于确保来自不同供应商代理的一致可观测性配置,并简化管理。因为这个协议是供应商中立的,这有助于确保一致的可观测性配置,并简化来自不同供应商代理的管理。

管理Pod中的自动监控

操作员会自动为你应用自动注入和配置仪器化,从而可以无需修改源代码即可收集遥测数据。若您的应用尚未用 OpenTelemetry 做好仪器化,这是一举两得的好方法,可以开始生成和收集应用程序遥测数据。

安装运算符

这可能看似显然,但在安装 Operator 之前,你必须有一个可以安装它的 Kubernetes 集群,并且需运行 Kubernetes 1.23+ 版本。请查看 兼容性矩阵 以获取具体版本要求。你可以在本地使用 minikubek0sKinD 等 Kubernetes 工具启动一个集群环境,或者使用云服务提供商的集群运行的集群。

接下来,这一点可能不太明显:你必须已经在该集群中安装了一个名为cert-manager的组件。这个组件通过确保这些证书有效且始终更新来管理Kubernetes证书。你可以通过kubectl或Helm chart来分别安装cert-manager和Operator。

小贴士:请记住,无论哪种情况,您都必须等待cert-manager安装完毕,之后再安装Operator;否则,Operator的安装都会失败。

如何使用 kubectl

运行以下命令来安装 cert-manager:

    kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.10.0/cert-manager.yaml

接下来,安装这个运营商。

kubectl apply -f https://github.com/open-telemetry/opentelemetry-operator/releases/latest/download/opentelemetry-operator.yaml
如何使用Helm

要安装 cert-manager,首先添加 Helm 仓库。

helm repo add jetstack https://charts.jetstack.io --force-update

执行这个命令来添加一个名为 jetstack 的 Helm 仓库,仓库地址为 https://charts.jetstack.io 并强制更新。

接下来,安装一下 cert-manager 的 Helm 图。

安装cert-manager组件 \   
helm install \   
cert-manager jetstack/cert-manager \   
--namespace cert-manager \   
--create-namespace \   
--version v1.16.1 \   
--set crds.enabled=true

预计前面的步骤可能需要一些时间。你可以通过按照此链接中的步骤验证cert-manager是否安装成功,或者通过运行下面的命令行检查部署状态:

在终端中输入:

kubectl get pods -namespace cert-manager

要安装 Operator,请注意需要 Helm 3.9+。首先,添加仓库源。

helm 仓库 add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts  # 添加 OpenTelemetry 仓库
helm 仓库 update  # 更新仓库列表

然后,安装插件

helm install --namespace opentelemetry-operator-system \   
  --create-namespace \   
  opentelemetry-operatoropen-telemetry/opentelemetry-operator
部署 OpenTelemetry 收集器,

在您的集群中设置了cert-manager和Operator之后,您可以安装Collector。Collector是一个多功能的组件,能够从各种来源收集遥测数据,并根据其配置进行多种转换,然后将处理后的数据导出到任何支持OpenTelemetry数据格式(OTLP,即OpenTelemetry协议)的后端。

收集器可以以几种不同的方式部署,这些方式被称为“部署模式”。你部署哪种或哪些模式取决于你的遥测需求和组织的资源。这里不深入讨论这个话题,但你可以通过此链接了解更多。

收集器自定义配置

自定义资源(CR)是一种特定的自定义,用于特定的Kubernetes安装;这种自定义通常在默认的Kubernetes安装中不可用。CR有助于使Kubernetes更具模块化。

操作员(Operator)有一个用于管理采集器部署的配置的CR,称为OpenTelemetryCollector。以下是一个示例OpenTelemetryCollector配置资源:

apiVersion: opentelemetry.io/v1beta1
kind: OpenTelemetryCollector
metadata:
  name: otelcol
  namespace: opentelemetry
spec:
  mode: statefulset
  config:
    receivers:
      otlp:
        protocols:
          grpc: {}
          http: {}
      prometheus:
        config:
          scrape_configs:
            - job_name: 'otel-collector'
              scrape间隔: 10s
              静态配置:
              - targets: [ '0.0.0.0:8888' ]
    processors:
      batch: {}
    exporters:
      logging:
        详细程度: detailed
    service:
      管道:
        traces:
          receivers: [otlp]
          processors: [batch]
          exporters: [logging]
        指标:
          receivers: [otlp, prometheus]
          processors: []
          exporters: [logging]
        logs:
          receivers: [otlp]
          processors: [batch]
          exporters: [logging]

对于如何实例化 OpenTelemetryCollector,有许多配置选项可供选择;然而,基本配置则需要如下内容:

  • mode,这些选项应为以下之一:deploymentsidecardaemonsetstatefulset。如果没有指定 mode,它将默认为 deployment
  • config,这个配置你可能看起来很熟悉,因为它就是Collector的配置文件。
常见共集电极配置部署问题及故障排除技巧

如果你没有看到你期望的数据,或者觉得哪里不对劲,请试试下面这些排查小技巧。

检查 Collector 的资源是否已正确部署

当你部署一个 OpenTelemetryCollector YAML 时,会创建以下这些对象:

1- OpenTelemetryCollector(用于收集和处理遥测数据的工具)

2- 收集器模块:

  • 如果你选择了非 sidecar 模式,查找名为 <collector_CR_name>-collector-<唯一标识符>DeploymentStatefulSetDaemonSet 资源。
  • 如果你选择了 sidecar 模式,应用 pod 中会有一个名为 otc-container 的 Collector sidecar 被创建。

3- 目标分配器 pod:

  • 如果开启了目标分配器,查找名为 <collector_CR_name>-targetallocator-<unique_identifier> 的资源。

收集器配置项 ConfigMap

  • 如果您选择的是非 Sidecar 模式,请查找名为 <collector_CR_name>-collector-<unique_identifier>DeploymentStatefulSet 等资源。
  • 如果您选择了 sidecar 模式,需要注意的是,Collector 配置作为环境变量被包含进去。

请确保在部署 OpenTelemetryCollector 时,前面提到的对象已经创建好。

首先,确认 OpenTelemetryCollector 资源已经成功部署。

kubectl get otelcol -n <namespace>
``` 命令用于获取指定命名空间中的 otelcol 实例。

当你使用 `OpenTelemetryCollector` 资源来部署 Collector 时,它会创建一个包含 Collector 配置 YAML 文件的 `ConfigMap`。确保 `ConfigMap` 是否在与 Collector 相同的命名空间中创建,并且检查这些配置是否正确。

列出你的 `ConfigMap` 项:
kubectl get configmap -n <namespace> | grep <collector-cr-name>-collector
运行该命令来查看名为<collector-cr-name>-collector的configmap。

我们也建议根据 Collector 的 `mode` 运行相应的命令来检查 Collector 的 pod。

* `deployment`、`StatefulSet`、`DaemonSet` 模式:
kubectl get pods -n <namespace> | grep <collector_cr_name>-collector

* `边车模式`(sidecar模式):

kubectl get pods <pod_name> -n opentelemetry -o jsonpath='{.spec.containers[*].name}' # 替换 <pod_name> 为具体的pod名称,此命令用于获取指定pod的容器名称列表。


这会列出在 pod 中创建的所有容器,包括 Collector 的边车容器。Collector 的配置信息作为环境变量中包含在 Collector 的边车容器中。

**查看Collector CR的版本号**

请查看一下你正在使用的 `OpenTelemetryCollector` CR 版本。这两个版本是:`v1alpha1`。

apiVersion: opentelemetry.io/v1alpha1
kind: OpenTelemetryCollector
metadata:
name: otelcol
namespace: opentelemetry
spec:
mode: statefulset
config: |
接收器配置如下:
receivers:
otlp:
protocols:
grpc:
http:

处理器配置如下:  
processors:  
  batch:  

导出器配置如下:  
exporters:  
  otlp:  
    endpoint: "<my_o11y_backend>"  
  logging:  
    verbosity: 详细  

服务管道配置如下:  
service:  
  pipelines:  
    traces:  
      接收器: [otlp]  
      处理器: [batch]  
      导出器: [otlp/ls, logging]  
    metrics:  
      接收器: [otlp, prometheus]  
      处理器:  
      导出器: [otlp/ls, logging]  
    logs:  
      接收器: [otlp]  
      处理器: [batch]  
      导出器: [otlp/ls, logging]

以及`v1beta1`:

apiVersion: opentelemetry.io/v1beta1
kind: OpenTelemetryCollector
metadata:
name: otelcol
namespace: opentelemetry
spec:
mode: statefulset
config:
接收器:
otlp:
协议:
grpc: {}
http: {}
处理器:
批处理: {}
导出器:
otlp:
端点: "<my_o11y_backend>"
日志:
详细程度: 详细
服务:
管道:
跟踪:
接收器: [otlp]
处理器: [批处理]
导出器: [otlp/ls, 日志]
度量:
接收器: [otlp, prometheus]
处理器: []
导出器: [otlp/ls, 日志]
日志:
接收器: [otlp]
处理器: [批处理]
导出器: [otlp/ls, 日志]


这两个API版本之间主要有两个主要区别。

1- `config` 部分是不同的;对于 `v1beta1`,配置值是以键值对的形式存在于 CR 配置中,相比之下,对于 `v1alpha1`,配置值是一个很长的文本字符串。需要注意的是,该文本字符串仍需遵循 YAML 格式。

2- 如果你使用的是 `v1beta1`,你就不能让 Collector 的配置值空着。你需要为单个值填写空花括号(`{}`),对于列表则填写空方括号(`[ ]`)。如果你用的是 `v1alpha1`,就不用这么操作了。

**检查Collector的基础图像**

默认情况下,`OpenTelemetryCollector` CR 使用 [核心发行版](https://github.com/open-telemetry/opentelemetry-collector) 的 Collector。核心发行版是简洁版的 Collector,供 OpenTelemetry 开发者开发和测试使用,包括一些基本组件,如扩展、连接器、接收器、处理器和导出器。

如果你想访问比核心提供的更多组件,你可以使用Collector的[Kubernetes发行版](https://github.com/open-telemetry/opentelemetry-collector-releases/tree/main/distributions/otelcol-k8s)。这个发行版是专门用来在Kubernetes集群中监控Kubernetes和运行在Kubernetes中的服务的。它包含核心和[contrib](https://github.com/open-telemetry/opentelemetry-collector-contrib)发行版中的一些组件。或者你可以[自己构建Collector发行版](https://opentelemetry.io/docs/collector/custom-collector/)。

你可以在 `spec.image` 中指定图片属性来设定收集器的基础图片,如:

apiVersion: opentelemetry.io/v1beta1
kind: OpenTelemetryCollector
metadata:
name: otelcol
namespace: opentelemetry
spec:
mode: statefulset
image: otel/opentelemetry-collector/contrib:0.102.1
config:
receivers:
otlp:
protocols:
grpc: {}
http: {}
processors:
batch: {}
exporters:
otlp:
endpoint: "<olly_backend_endpoint>"


**检查一下你的后端服务商的权限要求**

如果你使用后端供应商来收集你的遥测数据,你可能需要配置一个账户密钥或访问令牌或类似访问凭证,并确保保密处理这些信息。

如下,创建一个Kubernetes秘密并进行Base64编码处理,以便将其保密并防止其以明文形式出现。

apiVersion: v1
kind: Secret
metadata:
name: otel-collector-secret
namespace: opentelemetry
data:
ACCESS_TOKEN: <base64_encoded_token>
type: "Opaque"


请检查你的导出配置

确认你已经根据所在区域在导出器配置中设置了正确的[端点](https://docs.newrelic.com/docs/opentelemetry/best-practices/opentelemetry-otlp/#configure-endpoint-port-protocol)。

**当一切都不起作用时……看看 Kubernetes 事件。**

Kubernetes 事件提供了关于集群中各个组件发生情况的详细且按时间顺序的信息。要查看特定命名空间的事件,可以试试:
kubectl get 事件 -n <namespace>

将 `<namespace>` 替换为您实际使用的命名空间,其中部署了您的 OpenTelemetry Operator 和资源。

#  instruments

软件仪器化是指在软件中添加代码以生成遥测数据——日志、指标和跟踪的过程。您可以使用 OpenTelemetry 以几种方式为代码添加仪器化,主要分为两种方式:代码驱动的方法和无代码的方法。

基于代码的解决方案要求您手动使用OpenTelemetry API来监控您的代码。虽然这可能需要一定的时间和精力来实现,但这一选项可以让您获得深入的洞察,并进一步提高您的遥测数据的质量,因为您有很高的控制度来选择监控哪些代码部分以及如何监控。

如果你想在不修改代码的情况下进行插桩(或你无法修改源代码),你可以使用无代码解决方案(或自动插桩代理)。这种方法通过使用桩或字节码代理在运行时或编译时拦截你的代码,从而在依赖的第三方库和框架中添加跟踪和度量插桩。截至目前,自动插桩目前支持 Java、Python、.NET、JavaScript、PHP 和 Go。想了解更多关于无代码插桩的信息,可以查看 [此链接](https://opentelemetry.io/docs/concepts/instrumentation/zero-code/)。

你也可以同时使用这两种选项——一些用户选择从无代码代理程序开始,然后手动添加额外的监控代码,例如添加自定义属性或创建新跨度(span)。或者,OpenTelemetry 还提供了更多基于代码和无代码解决方案之外的选择。更多详情,请参阅[此链接](https://opentelemetry.io/docs/concepts/instrumentation/#additional-opentelemetry-benefits)。

## 零代码监控操作

操作符有一个名为 `Instrumentation` 的 CR(定制资源),可以自动将 OpenTelemetry 仪器注入并配置化到您的 Kubernetes Pod 中,从而为您的应用程序提供零代码仪器化的优势。目前,这支持以下应用环境:Apache HTTPD、.NET、Go、Java、nginx、Node.js 和 Python。

以下是一个 Python 服务程序的 `Instrumentation` 示例定义:

apiVersion: api 版本: opentelemetry.io/v1alpha1
kind: 类型: Instrumentation
metadata:
name: 名称: python-instrumentation
namespace: 命名空间: application
spec:
env:

  • name: 环境变量名称: OTEL_EXPORTER_OTLP_TIMEOUT
    value: 值: "20"
  • name: 环境变量名称: OTEL_TRACES_SAMPLER
    value: 值: parentbased_traceidratio
  • name: 环境变量名称: OTEL_TRACES_SAMPLER_ARG
    value: 值: "0.85"
    exporter:
    endpoint: 端点: http://localhost:4317
    propagators:
  • 传播器: tracecontext
  • 传播器: baggage
    sampler:
    type: 采样器类型: parentbased_traceidratio
    value: 采样器值: "0.25"
    python:
    env:
    • name: 环境变量名称: OTEL_METRICS_EXPORTER
      value: 值: otlp_proto_http
    • name: 环境变量名称: OTEL_LOGS_EXPORTER
      value: 值: otlp_proto_http
    • name: 环境变量名称: OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED
      value: 值: "true"
    • name: 环境变量名称: OTEL_EXPORTER_OTLP_ENDPOINT
      value: 值: http://localhost:4318

你可以使用单一的自动仪器化 YAML 来服务于多个用不同语言编写的多个服务(前提是它们支持自动仪器化功能)。将全局环境变量列在 spec.env 中,将特定语言的环境变量列在 spec.<语言名>.env 中。你可以在同一个 Instrumentation 资源中混合使用各种语言的环境变量配置。

为了利用操作员的自动仪器化功能,仅部署一个 Instrumentation 资源是不够的,还需要进一步的配置。自动仪器化配置必须与要进行仪器化的代码相关联。这可以通过在应用的 Deployment YAML 文件的模板定义部分添加自动仪器化注解来实现,例如:

apiVersion: apps/v1  
kind: Deployment   
metadata:  
  name: my-deployment-with-sidecar   
spec:  
  replicas: 1   
  selector:  
    matchLabels:  
      app: my-pod-with-sidecar  
  template:  
    metadata:   
      labels:  
        app: my-pod-with-sidecar  
      annotations:  
        sidecar.opentelemetry.io/inject: "true"  
        instrumentation.opentelemetry.io/inject-python: "true"  
    spec:  
      containers:  
        - name: py-otel-server  
          image: otel-python-lab:0.1.0-py-otel-server  
          ports:  
            - containerPort: 8082  
              name: py-server-port

当将 instrumentation.opentelemetry.io/inject-python 设置为 true 时,它会让 Operator 在此 Pod 中运行的容器中自动注入 Python 代码。对于其他语言,只需将 python 替换为相应的语言名称(例如,Java 应用程序使用 instrumentation.opentelemetry.io/inject-java)。可以将此值设为 false 来关闭自动注入。

如果你有多个 Instrumentation 资源,你需要指定使用哪个,否则运算符不会知道该选择哪个。你可以按照如下方式指定:

  • 按名称查找。当 Instrumentation 资源与 Deployment 在同一个命名空间时,请使用此方法。例如,instrumentation.opentelemetry.io/inject-java: my-instrumentation 将找到名为 my-instrumentationInstrumentation 资源。
  • 按命名空间和名称查找。当 Instrumentation 资源位于不同的命名空间时,请使用此方法。例如:instrumentation.opentelemetry.io/inject-java: my-namespace/my-instrumentation 将在命名空间 my-namespace 中找到名为 my-instrumentationInstrumentation 资源。

您必须在注释过的应用之前部署 Instrumentation 资源;否则,您的代码不会被自动插入代码监控。操作符(Operator)在应用的 pod 启动时通过添加一个 初始化容器(init container) 来注入自动代码监控,这意味着如果在您的服务部署之前 Instrumentation 资源不可用,自动代码监控将会失败。

常见仪器问题及故障排除技巧

如果你的收集器似乎没有在处理数据,或者你觉得自动监控不起作用,可以尝试以下步骤来排查和解决此问题。

检查一下仪器资源是否正确部署了

执行以下命令以确认您的Kubernetes集群中已经创建了Instrumentation资源。

运行以下命令来描述名为 `otelinst` 的资源,该资源位于指定的命名空间中:

kubectl describe otelinst -n <namespace>


**确认资源安排的顺序**

请确认在部署您的 `Deployment` 之前,您的 `Instrumentation` CR 已经部署。正如我们之前学到的,如果您是通过 Operator 自动注入 Instrumentation,必须在部署服务的 `Deployment` 资源之前部署 `Instrumentation` 资源,因为 `Deployment` 会创建一个 `init-container` 用于自动注入 Instrumentation。因此,当您运行以下命令时,您应该能看到一个用于自动注入的 init-container:
kubectl get pod <pod_name> -n <namespace> \  
  -o jsonpath='{.spec.initContainers[*].name}'
(此命令用于从指定的Pod中检索Kubernetes集群中的初始化容器名称。)

在Kubernetes集群中,`pod`可以翻译为`Pod`,`namespace`可以翻译为`命名空间`,`initContainers`可以翻译为`初始化容器`。

**检查你的自动标注中的CR注解**

确认注释里没有错别字。

确认它们在 pod 的元数据定义 (`spec.template.metadata.annotations`) 中,而不是在部署的元数据定义 (`metadata.annotations`) 中。

apiVersion: apps/v1
kind: Deployment
metadata:
name: py-otel-server
namespace: opentelemetry
labels:
app: my-app
app.kubernetes.io/name: py-otel-server
spec:
replicas: 1
selector:
matchLabels:
app: my-app
app.kubernetes.io/name: py-otel-server
template:
metadata:
labels:
app: my-app
app.kubernetes.io/name: py-otel-server
annotations:
instrumentation.opentelemetry.io/inject-python: "true"
spec:
containers:

  • name: py-otel-server
    image: otel-target-allocator-talk:0.1.0-py-otel-server
    imagePullPolicy: IfNotPresent
    ports:
    • containerPort: 8082
      name: py-server-port
      env:
      • name: OTEL_RESOURCE_ATTRIBUTES
        value: service.name=py-otel-server,service.version=0.1.0

请检查你的接口配置

端点(如以下示例中的 spec.exporter.endpoint 所配置的)指的是您在 Kubernetes 集群中遥测数据的目的地。

apiVersion: opentelemetry.io/v1alpha1
kind: Instrumentation
metadata:
  name: python-instrumentation
  namespace: opentelemetry
spec:
  # 配置导出器设置
  exporter:
    endpoint: http://otelcol-collector.opentelemetry.svc.cluster.local:4318
  env: 
  # 配置传播器
  propagators:
    - tracecontext
    - baggage
  python:
    # 设置环境变量
    env:
      - name: OTEL_METRICS_EXPORTER
        value: console,otlp_proto_http
      - name: OTEL_LOGS_EXPORTER
        value: otlp_proto_http
      - name: OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED
        value: "true"

Instrumentation 资源中,spec.exporter.endpoint 配置项允许您定义 telemetry 数据的目的地。如果您不设置该配置项,则默认值为 http://localhost:4317

如果你将遥测发送到Collector,spec.exporter.endpoint 的值应为你的Collector Service 名称。

从上面的例子来看,otel-collector 是 OTel Collector Kubernetes Service

此外,如果Collector运行在不同的命名空间中,你必须在Collector服务名后面加上opentelemetry.svc.cluster.local,其中opentelemetry是我所部署的Collector所在命名空间。它可以是任意你选定的命名空间,例如。

最后,请确保你用的是正确的Collector端口。通常情况下,你可以选择 4317(gRPC)或 4318(HTTP);然而,对于Python自动仪器化追踪,你只能使用4318。有关Python自动仪器化的详情,请参阅此处。确认你使用的语言是否有类似的注意事项。

注意: 通常情况下,如果您将Collector作为Sidecar进行部署,您的端点地址应为[http://localhost:4317](http://localhost:4317)[http://localhost:4318](http://localhost:4318)(请记住:如果是Python版本,端口号必须是4318)。

当其他方法都不管用时……查看一下操作员日志

运行以下命令来检查日志中是否有任何 error 记录。

kubectl logs -l app.kubernetes.io/name=opentelemetry-operator \  
  --container manager \  
  -n opentelemetry-operator-system --follow

请注意,上述情况仅适用于您拥有Kubernetes集群管理员权限的情形。如果没有权限,您仍然可以通过查看Kubernetes事件日志来了解情况,就像我们之前排查OpenTelemetryCollector资源问题时的做法一样。

kubectl get events -n <namespace>

运行以上命令以获取指定命名空间中的事件:

概要

OpenTelemetry Operator 管理一个或多个采集器的部署和配置,并将无代码仪器解决方案注入并配置到您的 Kubernetes pod 中。这使您能够开始使用 OpenTelemetry 仪器,并通过在应用程序中添加手动仪器来进一步增强您的遥测。

在这篇博客文章中,你了解了 Operator 的各种细节,从常见的安装难题到解决自动插桩和 Collector 部署中的问题。通过详细的安装步骤和故障排除技巧,你现在可以有效地利用 Operator 来部署、配置和管理你的 Collectors,以及支持库的自动插桩。

这篇博客文章是根据我和Reese Lee在KubeCon North America 2024共同举办的Observability Day上的演讲整理的。你可以在这里观看我们的演讲录像。

下一步是

现在你已经知道了如何安装Operator并利用其功能,自己动手试一试,克隆这个仓库(this repo)。

这里还有一些推荐资源供您查看:,

最初发布于https://geekingoutpodcast.substack.com

注:链接中的原文保持不变,通常不翻译链接部分。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消