草庐IT

基于云原生的私有化 PaaS 平台交付实践

kubesphere 2023-03-28 原文

作者:牛玉富,某知名互联网公司专家工程师。喜欢开源 / 热衷分享,对 K8s 及 golang 网关有较深入研究。

本文将解读如何利用云原生解决私有化交付中的问题,进而打造一个 PaaS 平台,提升业务平台的复用性。在进入正题之前,有必要先明确两个关键词:

  • PaaS 平台:多个核心业务服务作为一个整体平台去封装,以平台形式提供服务。
  • 私有化交付:平台需要部署私有云环境中,要面对无网情况下依然可以运转。

传统交付痛点

如上图:私有云会有明确的安全性要求

  1. 私有云服务无法连接外网,数据只能通过单向网闸形式进行摆渡到内网私有云。
  2. 源代码只能存储在公司机房中,私有云只部署编译文件。
  3. 服务会不定期迭代,另外为了保证服务稳定性需要自建独立业务监控。

基于以上要求面临的挑战大概有几点:

  1. 架构可迁移性差:服务之间配置复杂,多种异构语言需要修改配置文件,无固定服务 DNS。
  2. 部署运维成本高:服务依赖环境需支持离线安装,服务更新需本地运维人员手动完成,复杂场景下,完整一次部署大概需要 数人 / 月 的时间。
  3. 监控运维成本高:监控需支持系统级 / 服务级 / 业务级监控,通知方式需支持短信、Webhook 等多种类型。

架构方案

我们的原则是 拥抱云原生和复用已有能力,近可能使用业界已存在且成熟技术方案。
我们采用 KubeSphere+K8S 作为服务编排,处于安全性及简洁性考虑对 Syncd 进行二次开发完整 DevOps 能力,监控系统上采用 Nightingale+Prometheus 方案。

如上图架构图

  1. 蓝色框内是我们底层 PaaS 集群,我们对业务服务通用服务统一进行了服务编排升级,用以解决架构迁移性差问题。
  2. 红色框内,监控系统作为一种编排服务形式存在,所有监控项交付前配置好。用以解决监控系统运维成本高问题。
  3. 紫色框内,服务容器可以实现跨网段自动拉取并自动化部署。用以解决服务服务部署成本高问题。

下面我们针对这三部分做下介绍。

服务编排:KubeSphere

KubeSphere 的愿景是打造一个以 K8s 为内核的云原生分布式操作系统,它的架构可以非常方便地使第三方应用与云原生生态组件进行即插即用(plug-and-play)的集成,支持云原生应用在多云与多集群的统一分发和运维管理,同时它还拥有活跃的社区。

KubeSphere 选型理由有以下几点:

基于制品的方式定制自己的私有化交付方案

私有化镜像文件打包

创建制品清单 :

apiVersion: kubekey.kubesphere.io/v1alpha2
kind: Manifest
metadata:
  name: sample
spec:
  arches:
  - amd64
...
  - type: kubernetes
    version: v1.21.5
  components:
    helm:
      version: v3.6.3
    cni:
      version: v0.9.1
    etcd:
      version: v3.4.13
    containerRuntimes:
    - type: docker
      version: 20.10.8
    crictl:
      version: v1.22.0
    harbor:
      version: v2.4.1
    docker-compose:
      version: v2.2.2
  images:
  - dockerhub.kubekey.local/kubesphere/kube-apiserver:v1.22.1
...

然后我们就可以通过命令进行导出了。

$ ./kk artifact export -m manifest-sample.yaml -o kubesphere.tar.gz

私有化部署

创建部署清单:

apiVersion: kubekey.kubesphere.io/v1alpha2
kind: Cluster
metadata:
  name: sample
spec:
  hosts:
  - {name: kubesphere01.ys, address: 10.89.3.12, internalAddress: 10.89.3.12, user: kubesphere, password: "Kubesphere123"}
  - {name: kubesphere02.ys, address: 10.74.3.25, internalAddress: 10.74.3.25, user: kubesphere, password: "Kubesphere123"}
  - {name: kubesphere03.ys, address: 10.86.3.66, internalAddress: 10.86.3.66, user: kubesphere, password: "Kubesphere123"}
  - {name: kubesphere04.ys, address: 10.86.3.67, internalAddress: 10.86.3.67, user: kubesphere, password: "Kubesphere123"}
  - {name: kubesphere05.ys, address: 10.86.3.11, internalAddress: 10.86.3.11, user: kubesphere, password: "Kubesphere123"}
  roleGroups:
    etcd:
    - kubesphere01.py
    - kubesphere02.py
    - kubesphere03.py
    control-plane:
    - kubesphere01.py
    - kubesphere02.py
    - kubesphere03.py
    worker:
    - kubesphere05.py
    registry:
    - kubesphere04.py
  controlPlaneEndpoint:
    internalLoadbalancer: haproxy
    domain: lb.kubesphere.local
    address: ""
    port: 6443
  kubernetes:
    version: v1.21.5
    clusterName: cluster.local
  network:
    plugin: calico
    kubePodsCIDR: 10.233.64.0/18
    kubeServiceCIDR: 10.233.0.0/18
    multusCNI:
      enabled: false
  registry:
    type: harbor
    auths:
      "dockerhub.kubekey.local":
        username: admin
        password: Kubesphere123
...

执行安装部署:

$ ./kk create cluster -f config-sample.yaml -a kubesphere.tar.gz --with-packages --with-kubesphere --skip-push-images

原来大量复杂的 K8s 部署、高可用方案、Harbor 私有化镜像仓库等,均可以完成自动化安装,极大的简化了私有化交付场景下 K8s 组件部署难度。

可视化界面极大简化操作流程

  • 创建部署:流水线式创建一个容器服务的部署、存储、服务访问。

  • 资源限制:限制容器的资源利用率 & 限制租户资源利用率。

  • 远程登陆:容器远程登陆功能。

基于 KubeSphere 的业务部署经验分享

私有化场景构建高可用服务实例部署,保障单实例挂掉不影响整体使用,我们要保证以下几点。

1、由于服务都需要有固定的网络标识和存储,所以我们需要创建 “有状态副本集部署”。

apiVersion: apps/v1
kind: StatefulSet
metadata:
  namespace: project
  name: ${env_project_name}
  labels:
    app: ${env_project_name}
spec:
  serviceName: ${env_project_name}
  replicas: 1
  selector:
    matchLabels:
      app: ${env_project_name}
  template:
    metadata:
      labels:
        app: ${env_project_name}
    spec:
      containers:
        - name: ${env_project_name}
          image: ${env_image_path}
          imagePullPolicy: IfNotPresent

2、有状态副本集使用 host 反亲和性保证服务分散到不同 host 中。

....
affinity:
   podAntiAffinity:
     preferredDuringSchedulingIgnoredDuringExecution:
       - weight: 100
         podAffinityTerm:
           labelSelector:
             matchExpressions:
               - key: app
                 operator: In
                 values:
                   - ${env_project_name}
           topologyKey: kubernetes.io/hostname
....

3、服务与服务之间互相调用均使用 K8s 底层的 DNS 进行配置。

4、集群内部依赖外部资源时需要设置为 Service,然后在内部提供服务。

kind: Endpoints
apiVersion: v1
metadata:
  name: redis-cluster
  namespace: project
subsets:
  - addresses:
      - ip: 10.86.67.11
    ports:
      - port: 6379
---
kind: Service
apiVersion: v1
metadata:
  name: redis-cluster
  namespace: project
spec:
  ports:
    - protocol: TCP
      port: 6379
      targetPort: 6379

5、借助 nip.io 域名实现服务动态域名解析调试。
nip.io 可以自动根据请求的域名中设置 IP 信息,完成响应的 IP 信息映射。

$ nslookup abc-service.project.10.86.67.11.nip.io
Server:         169.254.25.10
Address:        169.254.25.10:53

Non-authoritative answer:
Name:   abc-service.project.10.86.67.11.nip.io
Address: 10.86.67.11

因此我们可以在构建 Ingress 时直接使用该域名:

---
kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
  name: gatekeeper
  namespace: project
spec:
  rules:
    - host: gatekeeper.project.10.86.67.11.nip.io
      http:
        paths:
          - path: /
            pathType: ImplementationSpecific
            backend:
              service:
                name: gatekeeper
                port:
                  number: 8000

6、挂载目录到宿主机,有时候需要容器直接关联宿主机目录具体操作如下。

...
spec:
    spec:
...
          volumeMounts:
            - name: vol-data
              mountPath: /home/user/data1
      volumes:
        - name: vol-data
          hostPath:
            path: /data0

7、有状态部署工作负载,主要涉及 StatefulSet、Service、volumeClaimTemplates、Ingress,示例如下:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  namespace: project
  name: gatekeeper
  labels:
    app: gatekeeper
spec:
  serviceName: gatekeeper
  replicas: 1
  selector:
    matchLabels:
      app: gatekeeper
  template:
    metadata:
      labels:
        app: gatekeeper
    spec:
      containers:
        - name: gatekeeper
          image: dockerhub.kubekey.local/project/gatekeeper:v362
          imagePullPolicy: IfNotPresent
          ports:
            - name: http-8000
              containerPort: 8000
              protocol: TCP
            - name: http-8080
              containerPort: 8080
              protocol: TCP
          resources:
            limits:
              cpu: '2'
              memory: 4Gi
          volumeMounts:
            - name: vol-data
              mountPath: /home/user/data1
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                labelSelector:
                  matchExpressions:
                    - key: app
                      operator: In
                      values:
                        - gatekeeper
                topologyKey: kubernetes.io/hostname
  volumeClaimTemplates:
    - metadata:
        name: vol-data
      spec:
        accessModes: [ "ReadWriteOnce" ]
        resources:
          requests:
            storage: 10Gi
---
apiVersion: v1
kind: Service
metadata:
  name: gatekeeper
  namespace: project
  labels:
    app: gatekeeper
spec:
  ports:
    - name: "http-8000"
      protocol: TCP
      port: 8000
      targetPort: 8000
    - name: "http-8080"
      protocol: TCP
      port: 8080
      targetPort: 8080
  selector:
    app: gatekeeper
  type: NodePort
---
kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
  name: gatekeeper
  namespace: project
spec:
  rules:
    - host: gatekeeper.project.10.86.67.11.nip.io
      http:
        paths:
          - path: /
            pathType: ImplementationSpecific
            backend:
              service:
                name: gatekeeper
                port:
                  number: 8000
    - host: gatekeeper.project.10.86.68.66.nip.io
      http:
        paths:
          - path: /
            pathType: ImplementationSpecific
            backend:
              service:
                name: gatekeeper
                port:
                  number: 8080

DevOps:基于 Syncd 构建服务自动化交付

DevOps 选型有很多,这里我们没有采用 Jenkins、GitRunner 等等,而是使用了我们团队内部比较熟悉的 Syncd 进行二次开发。原因有两点:

  1. 处于安全考虑:我们的源码无法在本地存放,所以基于 gitlab 构建打包的方案,对我们用处不是很大,使用是一种资源浪费。
  2. 功能简洁性:虽然 Syncd 已经停更 2 年多但是,但其核心的 CICD 功能比较完善且前后端拓展性强,我们可以很轻松拓展相应的功能。

Syncd 核心思路:

  1. 从使用本地工具链构建打包镜像,这里可以把 docker push 当作 git push 理解。
  2. 通过 Syncd 拉取镜像包完成部署流程打包上线操作,通过打包时设置版本号便于服务回滚。

构建本地工具链

1、基于项目创建目录

#创建目录
cd /Users/niuyufu/goproject/abc-service
mkdir -p devops
cd devops

2、导入 Dockerfile,大家可基于业务自行创建。
3、创建 tool.sh 文件

cat >> tool.sh << EOF
#!/bin/sh
 
###########配置区域##############
 
#模块名称,可变更
module=abc-service
#项目名称
project=project1
#容器名称
container_name=${project}"_"${module}
#镜像名称
image_name=${project}"/"${module}
#服务端口映射:宿主机端口:容器端口,多个逗号间隔
port_mapping=8032:8032
#镜像hub地址
image_hub=dockerhub.kubekey.local
#镜像tag
image_tag=latest
 
###########配置区域##############
 
#构建工具
action=$1
case $action in
"docker_push")
  image_path=${image_hub}/${image_name}:${image_tag}
  docker tag ${image_name}:${image_tag} ${image_path}
  docker push ${image_path}
  echo "镜像推送完毕,image_path: "${image_path}
  ;;
"docker_login")
  container_id=$(docker ps -a | grep ${container_name} | awk '{print $1}')
  docker exec -it ${container_id} /bin/sh
  ;;
"docker_stop")
  docker ps -a | grep ${container_name} | awk '{print $1}' | xargs docker stop
  container_id=`docker ps -a | grep ${container_name} | awk '{print $1}' | xargs docker rm`
  if [ "$container_id" != "" ];then
    echo "容器已关闭,container_id: "${container_id}
  fi
 
  if [ "$images_id" != "" ];then
    docker rmi ${images_id}
  fi
 
  ;;
"docker_run")
  docker ps -a | grep ${container_name} | awk '{print $1}' | xargs docker stop
  docker ps -a | grep ${container_name} | awk '{print $1}' | xargs docker rm
  port_mapping_array=(${port_mapping//,/ })
  # shellcheck disable=SC2068
  for var in ${port_mapping_array[@]}; do
    port_mapping_str=${mapping_str}" -p "${var}
  done
  container_id=$(docker run -d ${port_mapping_str} --name=${container_name} ${image_name})
  echo "容器已启动,container_id: "${container_id}
  ;;
"docker_build")
  if [ ! -d "../output" ]; then
    echo "../output 文件夹不存在,请先执行 ../build.sh"
    exit 1
  fi
  cp -rf ../output ./
  docker build -f Dockerfile -t ${image_name} .
  rm -rf ./output
  echo "镜像编译成功,images_name: "${image_name}
  ;;
*)
  echo "可运行命令:
docker_build    镜像编译,依赖../output 文件夹
docker_run      容器启动,依赖 docker_build
docker_login    容器登陆,依赖 docker_run
docker_push     镜像推送,依赖 docker_build"
  exit 1
  ;;
esac
EOF

4、执行项目打包,请确保产出物在 ./output 中

$cd ~/goproject/abc-service/
$sh build.sh
abc-service build ok
make output ok
build done

5、利用 tool.sh 工具进行服务调试

tools.sh 执行顺序一般是这样的:./output 产出物→docker_build→docker_run→docker_login→docker_push

$cd devops
$chmod +x tool.sh
#查看可运行命令
$sh tool.sh
可运行命令:
docker_build    镜像编译,依赖../output 文件夹
docker_run      容器启动,依赖 docker_build
docker_login    容器登陆,依赖 docker_run
docker_push     镜像推送,依赖 docker_build
 
 
#docker_build举例:
$sh tool.sh docker_build
[+] Building 1.9s (10/10) FINISHED
 => [internal] load build definition from Dockerfile                                                                                      0.1s
 => => transferring dockerfile: 37B                                                                                                       0.0s
 => [internal] load .dockerignore                                                                                                         0.0s
 => => transferring context: 2B
...                                                                   0.0s
 => exporting to image                                                                                                                    0.0s
 => => exporting layers                                                                                                                   0.0s
 => => writing image sha256:0a1fba79684a1a74fa200b71efb1669116c8dc388053143775aa7514391cdabf                                              0.0s
 => => naming to docker.io/project/abc-service                                                                                         0.0s
 
Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
镜像编译成功,images_name: project/abc-service
 
 
#docker_run举例:
$ sh tool.sh docker_run
6720454ce9b6
6720454ce9b6
容器已启动,container_id: e5d7c87fa4de9c091e184d98e98f0a21fd9265c73953af06025282fcef6968a5
 
 
#可以使用 docker_login 登陆容器进行代码调试:
$ sh tool.sh docker_login
sh-4.2# sudo -i
root@e5d7c87fa4de:~$
 
 
#docker_push举例:
$sh tool.sh docker_push                                                                                                              130 ↵
The push refers to repository [dockerhub.kubekey.local/citybrain/gatekeeper]
4f3c543c4f39: Pushed
54c83eb651e3: Pushed
e4df065798ff: Pushed
26f8c87cc369: Pushed
1fcdf9b8f632: Pushed
c02b40d00d6f: Pushed
8d07545b8ecc: Pushed
ccccb24a63f4: Pushed
30fe9c138e8b: Pushed
6ceb20e477f1: Pushed
76fbea184065: Pushed
471cc0093e14: Pushed
616b2700922d: Pushed
c4af1604d3f2: Pushed
latest: digest: sha256:775e7fbabffd5c8a4f6a7c256ab984519ba2f90b1e7ba924a12b704fc07ea7eb size: 3251
镜像推送完毕,image_path: dockerhub.kubekey.local/citybrain/gatekeeper:latest

#最后登陆Harbor测试镜像是否上传
https://dockerhub.kubekey.local/harbor/projects/52/repositories/gatekeeper

基于 Syncd 进行服务打包构建

1、项目配置

新增项目

设置 tool.sh 中生成的镜像地址。

设置构建脚本。

参照有状态工作负载填写构建脚本。

2、创建上线单

3、构建部署包执行部署

4、切换到 KubeSphere 查看部署效果。

至此已完成 DevOps 与 KubeSphere 的功能打通。

服务监控:基于 Nightingale 构建企业级监控

选型理由

  1. 可视化引擎:内置模板,开箱即用。

  1. 告警分析引擎:灵活管理、告警自愈、开箱即用。

  1. 支持 Helm Chart 一键完成应用及服务部署,私有化场景中我们只需要关心容器融合本地化即可。
$ git clone https://github.com/flashcatcloud/n9e-helm.git
$ helm install nightingale ./n9e-helm -n n9e --create-namespace

实际规则配置演示

  1. 配置告警规则,无缝支持 PromQL 灵活编写各种规则。

  1. 配置告警接收组

  1. 实际接收告警消息及恢复消息



总结

私有化交付下因业务场景不同,对云原生的应用选型也不相同。本文仅对我们自身业务场景做了介绍,如有问题欢迎指正,另外其他场景下的云原生应用也随时欢迎大家来和我交流探讨。

本文由博客一文多发平台 OpenWrite 发布!

有关基于云原生的私有化 PaaS 平台交付实践的更多相关文章

  1. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

    类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

  2. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  3. ruby - 具有身份验证的私有(private) Ruby Gem 服务器 - 2

    我想安装一个带有一些身份验证的私有(private)Rubygem服务器。我希望能够使用公共(public)Ubuntu服务器托管内部gem。我读到了http://docs.rubygems.org/read/chapter/18.但是那个没有身份验证-如我所见。然后我读到了https://github.com/cwninja/geminabox.但是当我使用基本身份验证(他们在他们的Wiki中有)时,它会提示从我的服务器获取源。所以。如何制作带有身份验证的私有(private)Rubygem服务器?这是不可能的吗?谢谢。编辑:Geminabox问题。我尝试“捆绑”以安装新的gem..

  4. 叮咚买菜基于 Apache Doris 统一 OLAP 引擎的应用实践 - 2

    导读:随着叮咚买菜业务的发展,不同的业务场景对数据分析提出了不同的需求,他们希望引入一款实时OLAP数据库,构建一个灵活的多维实时查询和分析的平台,统一数据的接入和查询方案,解决各业务线对数据高效实时查询和精细化运营的需求。经过调研选型,最终引入ApacheDoris作为最终的OLAP分析引擎,Doris作为核心的OLAP引擎支持复杂地分析操作、提供多维的数据视图,在叮咚买菜数十个业务场景中广泛应用。作者|叮咚买菜资深数据工程师韩青叮咚买菜创立于2017年5月,是一家专注美好食物的创业公司。叮咚买菜专注吃的事业,为满足更多人“想吃什么”而努力,通过美好食材的供应、美好滋味的开发以及美食品牌的孵

  5. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  6. kvm虚拟机安装centos7基于ubuntu20.04系统 - 2

    需求:要创建虚拟机,就需要给他提供一个虚拟的磁盘,我们就在/opt目录下创建一个10G大小的raw格式的虚拟磁盘CentOS-7-x86_64.raw命令格式:qemu-imgcreate-f磁盘格式磁盘名称磁盘大小qemu-imgcreate-f磁盘格式-o?1.创建磁盘qemu-imgcreate-fraw/opt/CentOS-7-x86_64.raw10G执行效果#ls/opt/CentOS-7-x86_64.raw2.安装虚拟机使用virt-install命令,基于我们提供的系统镜像和虚拟磁盘来创建一个虚拟机,另外在创建虚拟机之前,提前打开vnc客户端,在创建虚拟机的时候,通过vnc

  7. ruby-on-rails - Rails 中同一个类的多个关联的最佳实践? - 2

    我认为我的问题最好用一个例子来描述。假设我有一个名为“Thing”的简单模型,它有一些简单数据类型的属性。像...Thing-foo:string-goo:string-bar:int这并不难。数据库表将包含具有这三个属性的三列,我可以使用@thing.foo或@thing.bar之类的东西访问它们。但我要解决的问题是当“foo”或“goo”不再包含在简单数据类型中时会发生什么?假设foo和goo代表相同类型的对象。也就是说,它们都是“Whazit”的实例,只是数据不同。所以现在事情可能看起来像这样......Thing-bar:int但是现在有一个新的模型叫做“Whazit”,看起来

  8. ruby-on-rails - 向 Rails 3 添加 Ruby 扩展方法的最佳实践? - 2

    我有一个要在我的Rails3项目中使用的数组扩展方法。它应该住在哪里?我有一个应用程序/类,我最初把它放在(array_extensions.rb)中,在我的config/application.rb中我加载路径:config.autoload_paths+=%W(#{Rails.root}/应用程序/类)。但是,当我转到railsconsole时,未加载扩展。是否有一个预定义的位置可以放置我的Rails3扩展方法?或者,一种预先定义的方式来添加它们?我知道Rails有自己的数组扩展方法。我应该将我的添加到active_support/core_ext/array/conversion

  9. Ruby - 如何处理子类意外覆盖父类(super class)私有(private)字段的问题? - 2

    假设您编写了一个类Sup,我决定将其扩展为SubSup。我不仅需要了解你发布的接口(interface),还需要了解你的私有(private)字段。见证这次失败:classSupdefinitialize@privateField="fromsup"enddefgetXreturn@privateFieldendendclassSub问题是,解决这个问题的正确方法是什么?看起来子类应该能够使用它想要的任何字段而不会弄乱父类(superclass)。编辑:equivalentexampleinJava返回"fromSup",这也是它应该产生的答案。 最佳答案

  10. ruby - 从另一个私有(private)方法中使用 self.xxx() 调用私有(private)方法 xxx,导致错误 "private method ` xxx' called” - 2

    我正在尝试获得良好的Ruby编码风格。为防止意外调用具有相同名称的局部变量,我总是在适当的地方使用self.。但是现在我偶然发现了这个:classMyClass上面的代码导致错误privatemethodsanitize_namecalled但是当删除self.并仅使用sanitize_name时,它会起作用。这是为什么? 最佳答案 发生这种情况是因为无法使用显式接收器调用私有(private)方法,并且说self.sanitize_name是显式指定应该接收sanitize_name的对象(self),而不是依赖于隐式接收器(也是

随机推荐