文档章节

cluster-proportional-autoscaler源码分析及如何解决KubeDNS性能瓶颈

WaltonWang
 WaltonWang
发布于 2017/11/30 01:57
字数 2815
阅读 386
收藏 3
点赞 0
评论 0

Author: xidianwangtao@gmail.com

工作机制

cluster-proportional-autoscaler是kubernetes的孵化项目之一,用来根据集群规模动态的扩缩容指定的namespace下的target(只支持RC, RS, Deployment),还不支持对StatefulSet。目前只提供两种autoscale模式,一种是linear,另一种是ladder,你能很容易的定制开发新的模式,代码接口非常清晰。

cluster-proportional-autoscaler工作机制很简单,每隔一定时间(通过--poll-period-seconds配置,默认10s)重复进行如下操作:

  • 统计一次集群中ScheduableNodes和ScheduableCores;
  • 从apiserver中获取最新configmap数据;
  • 根据对应的autoscale模式,进行configmap参数解析;
  • 据对应的autoscale模式,计算新的期望副本数;
  • 如果与上一次期望副本数不同,则调用Scale接口触发AutoScale;

配置说明

cluster-proportional-autoscaler一共有下面6项flag:

  • --namespace: 要autoscale的对象所在的namespace;
  • --target: 要autoscale的对象,只支持deployment/replicationcontroller/replicaset,不区分大小写;
  • --configmap: 配置实现创建好的configmap,里面存储要使用的模式及其配置,后面会有具体的示例;
  • --default-params: 如果--configmap中配置的configmap不存在或者后来被删除了,则使用该配置来创建新的configmap,建议要配置;
  • --poll-period-seconds: 检查周期,默认为10s。
  • --version: 打印vesion并退出。

源码分析

pollAPIServer

pkg/autoscaler/autoscaler_server.go:82
func (s *AutoScaler) pollAPIServer() {
	// Query the apiserver for the cluster status --- number of nodes and cores
	clusterStatus, err := s.k8sClient.GetClusterStatus()
	if err != nil {
		glog.Errorf("Error while getting cluster status: %v", err)
		return
	}
	glog.V(4).Infof("Total nodes %5d, schedulable nodes: %5d", clusterStatus.TotalNodes, clusterStatus.SchedulableNodes)
	glog.V(4).Infof("Total cores %5d, schedulable cores: %5d", clusterStatus.TotalCores, clusterStatus.SchedulableCores)

	// Sync autoscaler ConfigMap with apiserver
	configMap, err := s.syncConfigWithServer()
	if err != nil || configMap == nil {
		glog.Errorf("Error syncing configMap with apiserver: %v", err)
		return
	}

	// Only sync updated ConfigMap or before controller is set.
	if s.controller == nil || configMap.ObjectMeta.ResourceVersion != s.controller.GetParamsVersion() {
		// Ensure corresponding controller type and scaling params.
		s.controller, err = plugin.EnsureController(s.controller, configMap)
		if err != nil || s.controller == nil {
			glog.Errorf("Error ensuring controller: %v", err)
			return
		}
	}

	// Query the controller for the expected replicas number
	expReplicas, err := s.controller.GetExpectedReplicas(clusterStatus)
	if err != nil {
		glog.Errorf("Error calculating expected replicas number: %v", err)
		return
	}
	glog.V(4).Infof("Expected replica count: %3d", expReplicas)

	// Update resource target with expected replicas.
	_, err = s.k8sClient.UpdateReplicas(expReplicas)
	if err != nil {
		glog.Errorf("Update failure: %s", err)
	}
}

GetClusterStatus

GetClusterStatus用于统计集群中SchedulableNodes, SchedulableCores,用于后面计算新的期望副本数。

pkg/autoscaler/k8sclient/k8sclient.go:142
func (k *k8sClient) GetClusterStatus() (clusterStatus *ClusterStatus, err error) {
	opt := metav1.ListOptions{Watch: false}

	nodes, err := k.clientset.CoreV1().Nodes().List(opt)
	if err != nil || nodes == nil {
		return nil, err
	}
	clusterStatus = &ClusterStatus{}
	clusterStatus.TotalNodes = int32(len(nodes.Items))
	var tc resource.Quantity
	var sc resource.Quantity
	for _, node := range nodes.Items {
		tc.Add(node.Status.Capacity[apiv1.ResourceCPU])
		if !node.Spec.Unschedulable {
			clusterStatus.SchedulableNodes++
			sc.Add(node.Status.Capacity[apiv1.ResourceCPU])
		}
	}

	tcInt64, tcOk := tc.AsInt64()
	scInt64, scOk := sc.AsInt64()
	if !tcOk || !scOk {
		return nil, fmt.Errorf("unable to compute integer values of schedulable cores in the cluster")
	}
	clusterStatus.TotalCores = int32(tcInt64)
	clusterStatus.SchedulableCores = int32(scInt64)
	k.clusterStatus = clusterStatus
	return clusterStatus, nil
}
  • Nodes数量统计时,是会剔除掉那些 Unschedulable Nodes的。
  • Cores数量统计时,是会减掉那些 Unschedulable Nodes对应Capacity。
    • 请注意,这里计算Cores时统计的是Node的Capacity,而不是Allocatable。
    • 我认为,使用Allocatable要比Capacity更好。
    • 这两者在大规模集群时就会体现出差别了,比如每个Node Allocatable比Capacity少1c4g,那么2K个Node集群规模时,就相差2000c8000g,这将是的target object number相差很大。

有些同学可能要问:Node Allocatable和Capacity有啥不同呢?

  • Capacity是Node硬件层面提供的全部资源,服务器配置的多少内存,cpu核数等,都是由硬件决定的。
  • Allocatable则要在Capacity的基础上减去kubelet flag中配置的kube-resreved和system-reserved资源大小,是Kubernetes给应用真正可分配的资源数。

syncConfigWithServer

syncConfigWithServer主要是从apiserver中获取最新configmap数据,注意这里并没有去watch configmap,而是按照--poll-period-seconds(默认10s)定期的去get,所以默认会存在最多10s的延迟。

pkg/autoscaler/autoscaler_server.go:124
func (s *AutoScaler) syncConfigWithServer() (*apiv1.ConfigMap, error) {
	// Fetch autoscaler ConfigMap data from apiserver
	configMap, err := s.k8sClient.FetchConfigMap(s.k8sClient.GetNamespace(), s.configMapName)
	if err == nil {
		return configMap, nil
	}
	if s.defaultParams == nil {
		return nil, err
	}
	glog.V(0).Infof("ConfigMap not found: %v, will create one with default params", err)
	configMap, err = s.k8sClient.CreateConfigMap(s.k8sClient.GetNamespace(), s.configMapName, s.defaultParams)
	if err != nil {
		return nil, err
	}
	return configMap, nil
}
  • 如果配置的--configmap在集群中已经存在,则从apiserver中获取最新的configmap并返回;
  • 如果配置的--configmap在集群中不存在,则根据--default-params的内容创建一个configmap并返回;
  • 如果配置的--configmap在集群中不存在,且--default-params又没有配置,则返回nil,意味着失败,整个流程结束,使用时请注意!

建议一定要配置--default-params,因为--configmap配置的configmap有可能有意或者无意的被管理员/用户删除了,而你又没配置--default-params,那么这个时候pollAPIServer将就此结束,因为着你没达到autoscale target的目的,关键是你可能并在不知道集群这个时候出现了这个情况。

EnsureController

EnsureController用来根据configmap中配置的controller type创建对应Controller及解析参数。

pkg/autoscaler/controller/plugin/plugin.go:32

// EnsureController ensures controller type and scaling params
func EnsureController(cont controller.Controller, configMap *apiv1.ConfigMap) (controller.Controller, error) {
	// Expect only one entry, which uses the name of control mode as the key
	if len(configMap.Data) != 1 {
		return nil, fmt.Errorf("invalid configMap format, expected only one entry, got: %v", configMap.Data)
	}
	for mode := range configMap.Data {
		// No need to reset controller if control pattern doesn't change
		if cont != nil && mode == cont.GetControllerType() {
			break
		}
		switch mode {
		case laddercontroller.ControllerType:
			cont = laddercontroller.NewLadderController()
		case linearcontroller.ControllerType:
			cont = linearcontroller.NewLinearController()
		default:
			return nil, fmt.Errorf("not a supported control mode: %v", mode)
		}
		glog.V(1).Infof("Set control mode to %v", mode)
	}

	// Sync config with controller
	if err := cont.SyncConfig(configMap); err != nil {
		return nil, fmt.Errorf("Error syncing configMap with controller: %v", err)
	}
	return cont, nil
}
  • 检查configmap data中是否只有一个entry,如果不是,则该configmap不合法,流程结束;
  • 检查controller的类型是否为linearladder其中之一,并调用对应的方法创建对应的Controller,否则返回失败;
    • linear --> NewLinearController
    • ladder --> NewLadderController
  • 调用对应Controller的SyncConfig解析configmap data中参数和configmap ResourceVersion更新到Controller对象中;

GetExpectedReplicas

linear和ladder Controller分别实现了自己的GetExpectedReplicas方法,用来计算期望此次监控到的数据应该有的副本数。具体的看下面关于Linear Controller和Ladder Controller部分。

UpdateReplicas

UpdateReplicas将GetExpectedReplicas计算得到的期望副本数,通过调用对应target(rc/rs/deploy)对应的Scale接口,由Scale去完成target的缩容扩容。

pkg/autoscaler/k8sclient/k8sclient.go:172
func (k *k8sClient) UpdateReplicas(expReplicas int32) (prevRelicas int32, err error) {
	scale, err := k.clientset.Extensions().Scales(k.target.namespace).Get(k.target.kind, k.target.name)
	if err != nil {
		return 0, err
	}
	prevRelicas = scale.Spec.Replicas
	if expReplicas != prevRelicas {
		glog.V(0).Infof("Cluster status: SchedulableNodes[%v], SchedulableCores[%v]", k.clusterStatus.SchedulableNodes, k.clusterStatus.SchedulableCores)
		glog.V(0).Infof("Replicas are not as expected : updating replicas from %d to %d", prevRelicas, expReplicas)
		scale.Spec.Replicas = expReplicas
		_, err = k.clientset.Extensions().Scales(k.target.namespace).Update(k.target.kind, scale)
		if err != nil {
			return 0, err
		}
	}
	return prevRelicas, nil
}

下面是对Linear Controller和Ladder Controller具体实现的代码分析。

Linear Controller

先来看看linear Controller的参数:

pkg/autoscaler/controller/linearcontroller/linear_controller.go:50
type linearParams struct {
	CoresPerReplica           float64 `json:"coresPerReplica"`
	NodesPerReplica           float64 `json:"nodesPerReplica"`
	Min                       int     `json:"min"`
	Max                       int     `json:"max"`
	PreventSinglePointFailure bool    `json:"preventSinglePointFailure"`
}

写configmap时,参考如下:

kind: ConfigMap
apiVersion: v1
metadata:
  name: nginx-autoscaler
  namespace: default
data:
  linear: |-
    { 
      "coresPerReplica": 2,
      "nodesPerReplica": 1,
      "preventSinglePointFailure": true,
      "min": 1,
      "max": 100
    }

其他参数不多说,我想提的是PreventSinglePointFailure,字面意思是防止单点故障,是一个bool值,代码中没有进行显示的初始化,意味着默认为false。可以在对应的configmap data或者dafault-params中设置"preventSinglePointFailure": true,但设置为true后,如果schedulableNodes > 1,则会保证target's replicas至少为2,也就是防止了target单点故障。

pkg/autoscaler/controller/linearcontroller/linear_controller.go:101

func (c *LinearController) GetExpectedReplicas(status *k8sclient.ClusterStatus) (int32, error) {
	// Get the expected replicas for the currently schedulable nodes and cores
	expReplicas := int32(c.getExpectedReplicasFromParams(int(status.SchedulableNodes), int(status.SchedulableCores)))

	return expReplicas, nil
}

func (c *LinearController) getExpectedReplicasFromParams(schedulableNodes, schedulableCores int) int {
	replicasFromCore := c.getExpectedReplicasFromParam(schedulableCores, c.params.CoresPerReplica)
	replicasFromNode := c.getExpectedReplicasFromParam(schedulableNodes, c.params.NodesPerReplica)
	// Prevent single point of failure by having at least 2 replicas when
	// there are more than one node.
	if c.params.PreventSinglePointFailure &&
		schedulableNodes > 1 &&
		replicasFromNode < 2 {
		replicasFromNode = 2
	}

	// Returns the results which yields the most replicas
	if replicasFromCore > replicasFromNode {
		return replicasFromCore
	}
	return replicasFromNode
}

func (c *LinearController) getExpectedReplicasFromParam(schedulableResources int, resourcesPerReplica float64) int {
	if resourcesPerReplica == 0 {
		return 1
	}
	res := math.Ceil(float64(schedulableResources) / resourcesPerReplica)
	if c.params.Max != 0 {
		res = math.Min(float64(c.params.Max), res)
	}
	return int(math.Max(float64(c.params.Min), res))
}
  • 根据schedulableCores和configmap中的CoresPerReplica,按照如下公式计算得到replicasFromCore;
    • replicasFromCore = ceil( schedulableCores * 1/CoresPerReplica )
  • 根据schedulableNodes和configmap中的NodesPerReplica,按照如下公式计算得到replicasFromNode;
    • replicasFromNode = ceil( schedulableNodes * 1/NodesPerReplica ) )
  • 如果configmap中配置了min或者max,则必须保证replicas在min和max范围内;
    • replicas = min(replicas, max)
    • replicas = max(replicas, min)
  • 如果配置了preventSinglePointFailure为true并且schedulableNodes > 1,则根据前面提到的逻辑进行防止单点故障,replicasFromNode必须大于2;
    • replicasFromNode = max(2, replicasFromNode)
  • 返回replicasFromNode和replicasFromCore中的最大者作为期望副本数。

概括起来,linear controller计算replicas的公式为:

replicas = max( ceil( cores * 1/coresPerReplica ) , ceil( nodes * 1/nodesPerReplica ) )
replicas = min(replicas, max)
replicas = max(replicas, min)

Ladder Controller

下面是ladder Controller的参数结构:

pkg/autoscaler/controller/laddercontroller/ladder_controller.go:66
type paramEntry [2]int
type paramEntries []paramEntry
type ladderParams struct {
	CoresToReplicas paramEntries `json:"coresToReplicas"`
	NodesToReplicas paramEntries `json:"nodesToReplicas"`
}

写configmap时,参考如下:

kind: ConfigMap
apiVersion: v1
metadata:
  name: nginx-autoscaler
  namespace: default
data:
  ladder: |-
    { 
      "coresToReplicas":
      [
        [ 1,1 ],
        [ 3,3 ],
        [256,4],
        [ 512,5 ],
        [ 1024,7 ]
      ],
      "nodesToReplicas":
      [
        [ 1,1 ],
        [ 2,2 ],
        [100, 5],
        [200, 12]
      ]
    }

下面是ladder Controller对应的计算期望副本值的方法。

func (c *LadderController) GetExpectedReplicas(status *k8sclient.ClusterStatus) (int32, error) {
	// Get the expected replicas for the currently schedulable nodes and cores
	expReplicas := int32(c.getExpectedReplicasFromParams(int(status.SchedulableNodes), int(status.SchedulableCores)))

	return expReplicas, nil
}

func (c *LadderController) getExpectedReplicasFromParams(schedulableNodes, schedulableCores int) int {
	replicasFromCore := getExpectedReplicasFromEntries(schedulableCores, c.params.CoresToReplicas)
	replicasFromNode := getExpectedReplicasFromEntries(schedulableNodes, c.params.NodesToReplicas)

	// Returns the results which yields the most replicas
	if replicasFromCore > replicasFromNode {
		return replicasFromCore
	}
	return replicasFromNode
}

func getExpectedReplicasFromEntries(schedulableResources int, entries []paramEntry) int {
	if len(entries) == 0 {
		return 1
	}
	// Binary search for the corresponding replicas number
	pos := sort.Search(
		len(entries),
		func(i int) bool {
			return schedulableResources < entries[i][0]
		})
	if pos > 0 {
		pos = pos - 1
	}
	return entries[pos][1]
}
  • 根据schedulableCores在configmap中的CoresToReplicas定义的那个范围中,就选择预先设定的期望副本数。
  • 根据schedulableNodes在configmap中的NodesToReplicas定义的那个范围中,就选择预先设定的期望副本数。
  • 返回上面两者中的最大者作为期望副本数。

注意:

  • ladder模式下,没有防止单点故障的设置项,用户配置configmap时候要自己注意;
  • ladder模式下,没有NodesToReplicas或者CoresToReplicas对应的配置为空,则对应的replicas设为1;

比如前面举例的configmap,如果集群中schedulableCores=400(对应期望副本为4),schedulableNodes=120(对应期望副本为5),则最终的期望副本数为5.

使用kube-dns-autoscaler解决KubeDNS性能瓶颈问题

通过如下yaml文件创建kube-dns-autoscaler Deployment和configmap, kube-dns-autoscaler每个30s会进行一次副本数计算检查,并可能触发AutoScale。

kind: ConfigMap
apiVersion: v1
metadata:
  name: kube-dns-autoscaler
  namespace: kube-system
data:
  linear: |
    {
    "nodesPerReplica": 10,
    "min": 1,
    "max": 50,
    "preventSinglePointFailure": true
    } 

‐‐‐

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: kube-dns-autoscaler
  namespace: kube-system
spec:
  template:
    metadata:
      labels:
        k8s-app: kube-dns-autoscaler
    spec:
      imagePullSecrets:
      - name: harborsecret
      containers:
      - name: autoscaler
        image: registry.vivo.xyz:4443/bigdata_release/cluster_proportional_autoscaler_amd64:1.0.0
        resources:
          requests:
            cpu: "50m"
            memory: "100Mi"
        command:
        - /cluster-proportional-autoscaler
        - --namespace=kube-system
        - --configmap=kube-dns-autoscaler
        - --target=Deployment/kube-dns
        - --default-params={"linear":{"nodesPerReplica":10,"min":1}}
        - --logtostderr=true
        - --v=2

总结和展望

  • cluster-proportional-autoscaler代码很简单,工作机制也很单纯,我们希望用它根据集群规模来动态扩展KubeDNS,以解决TensorFlow on Kubernetes项目中大规模的域名解析性能问题。

  • 目前它只支持根据SchedulableNodes和SchedulableCores来autoscale,在AI的场景中,存在集群资源极度压榨的情况,一个集群承载的svc和pod波动范围很大,后续我们可能会开发根据service number来autoscale kubedns的controller。

  • 另外,我还考虑将KubeDNS的部署从AI训练服务器中隔离出来,因为训练时经常会将服务器cpu跑到95%以上,KubeDNS也部署在这台服务器上的话,势必也会影响KubeDNS性能。

© 著作权归作者所有

共有 人打赏支持
WaltonWang
粉丝 159
博文 88
码字总数 182269
作品 0
深圳
程序员
vivo基于Kubernetes构建企业级TaaS平台实践

Author: xidianwangtao@gmail.com vivo TaaS架构 关于如何将Kubernetes和TensorFlow整合起来的Topic,以及我们的CaaS技术栈的介绍,请参考过往的两篇文章,在这里我不再赘述。 浅尝TensorFlo...

WaltonWang ⋅ 01/11 ⋅ 0

k8s集群之kubernetes-dashboard和kube-dns组件部署安装

k8s集群之kubernetes-dashboard和kube-dns组件部署安装 说明 最好先部署kube-dns,有些组合服务直接主机用hostname解析,例如redis主从,heapster监控组件influxdb、grafana之间等。 参考文档...

minminmsn ⋅ 2017/02/23 ⋅ 0

伸缩Kubernetes到2500个节点中遇到的问题和解决方法

Kubernetes自从1.6起便号称可以承载5000个以上的节点,但是从数十到5000的路上,难免会遇到问题。 本片文章即分享Open API在kubernetes 5000之路上的经验,包括遇到的问题、尝试解决问题以及...

好雨云帮 ⋅ 04/24 ⋅ 0

kubernetes之部署kubedns

一,理解Kubedns原理 通俗理解,首先明白k8s dns是为服务的发现而生,即service的发现,为了能够让其它服务能够直接通过service 名字找到它们,于是就需要dns将service名转换为它的VIP, 那么...

zhoufei461 ⋅ 01/11 ⋅ 0

Kubernetes 1.5.8 和 1.5.9-beta.0,容器集群管理系统

Kubernetes 1.5.8 已发布,自 1.5.7 以来的变更: 升级 dnsmasq 至最新版本 (#53149, @bowei) 在 GCP 平台上,e2e 测试会记录集群有哪些 OS 镜像 (#48310, @abgworrall) 升级 cluster-propor...

局长 ⋅ 2017/10/02 ⋅ 0

Kubernetes Autoscaling是如何工作的?

Kubernetes Autoscaling是如何工作的?这是最近我们经常被问到的一个问题。 所以本文将从Kubernetes Autoscaling功能的工作原理以及缩放集群时可以提供的优势等方面进行解释。 什么是Autosca...

好雨云帮 ⋅ 05/07 ⋅ 0

深度学习基础设施

摘要:深度学习是一门依据经验的科学,基础设施的质量,能够对深度学习的效果起到非常大的作用。幸运的是,如今的开源生态系统,能让任何人都搭建起优秀的深度学习基础设施。在这篇文章中,我...

局长 ⋅ 2016/09/05 ⋅ 2

kubernetes1.10部署

一、环境准备 目前搭建仍然采用 5 台虚拟机测试,基本环境如下 IP Type Docker OS 192.168.1.61 master、node、etcd 18.03.0-ce ubuntu 16.04 192.168.1.62 master、node、etcd 18.03.0-ce u...

拎壶冲冲冲 ⋅ 04/28 ⋅ 0

kubernetes 1.8 单机部署

注:这里就不科普了,直接开始部署。[这里使用HTTP来部署] 1. 关闭 SeLinux 和 FireWall # sed -i "s/SELINUX=enforcing/SELINUX=disabled/g" /etc/selinux/config systemctl stop firewalld ......

结束的伤感 ⋅ 2017/10/23 ⋅ 0

深度学习的基础设施(英)

来自于【http://openai.org】 By Vicki Cheung, Jonas Schneider, Ilya Sutskever, and Greg Brockman August 29, 2016 Deep learning is an empirical science, and the quality of a grou......

openthings ⋅ 2016/10/11 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

180621-一个简单的时间窗口设计与实现

如何设计一个计数的时间窗口 时间窗口,通常对于一些实时信息展示中用得比较多,比如维持一个五分钟的交易明细时间窗口,就需要记录当前时间,到五分钟之前的所有交易明细,而五分钟之前的数...

小灰灰Blog ⋅ 31分钟前 ⋅ 0

Android之Dalvik、ART、JIT、AOT

Android之Dalvik、ART、JIT、AOT 本文内容:Dalvik、ART、JIT、AOT之间关系 本文定位:知识记录 学习过程记录,加深理解,提升文字组合表达能力。也希望能给学习的同学一些灵感 本文整理于[...

lichuangnk ⋅ 35分钟前 ⋅ 0

Thrift RPC实战(五) thrift连接池

Thrift本身没有提供连接池,我们可以用Apache Commons Pool2来实现一个 一、定义对象工厂 BasePooledObjectFactory<T> extends BaseObject implements PooledObjectFactory<T> public class......

lemonLove ⋅ 35分钟前 ⋅ 0

git 命令简写

简写 命令 g git gst git status gd git diff gdc git diff --cached gdv git diff -w "$@" | view - gl git pull gup git pull --rebase gp git push gc git commit -v gc! git commit -v ......

charley158 ⋅ 43分钟前 ⋅ 0

Java中的锁使用与实现

1.Lock接口 锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源。 在Lock出现之前,java程序是靠synchronized关键字实现锁功能的,而Java SE5之后,...

ZH-JSON ⋅ 44分钟前 ⋅ 0

Intellij IDEA神器常用技巧四-类和方法注释模板设置

IDEA自带的注释模板不是太好用,我本人到网上搜集了很多资料系统的整理了一下制作了一份比较完整的模板来分享给大家,我不是专业玩博客的,写这篇文章只是为了让大家省事。 这里设置的注释模...

Mkeeper ⋅ 47分钟前 ⋅ 0

Jira接入钉钉机器人

https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.9Z9czj&treeId=257&articleId=106075&docType=1...

谢思华 ⋅ 49分钟前 ⋅ 0

微信公众号开发

一、开通微信服务号,填写URL,微信将想你发送验证信息。接收并处理微信发来的GET请求 二、处理客户向公众号发送的各种消息: 如文本、图片、乐音、视频、音乐 : 消息分为:1 请求消息(文本...

无敌小学僧 ⋅ 55分钟前 ⋅ 0

广州三本找Java实习经历

前言 只有光头才能变强 这阵子跑去面试Java实习生啦~~~我来简单介绍一下背景吧。 广州三本大三在读,在广州找实习。大学开始接触编程,一个非常平庸的人。 在学习编程时,跟我类似的人应该会...

Java3y ⋅ 57分钟前 ⋅ 0

php json_encode()不转义中文字符 和 斜杠"/"

php格式化json的函数 json_encode($value,$options) 其中有2个比较常用到的参数 JSON_UNESCAPED_UNICODE (中文不转为unicode ,对应的数字 256) JSON_UNESCAPED_SLASHES (不转义斜杠,对应的...

一只大橘子 ⋅ 今天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部