1、部署一个service应用

在部署第一个Knative Service之前,我们先了解一下它的部署模型和对应的Kubernetes资源。如图6-2所示,在部署Knative Serving Service的过程中,Knative Serving控制器将创建configuration、Revision和Route三个资源对象。

配置(configuration):Knative configuration维护了部署的目标状态,提供了一个干净的代码和配置分离、遵循12要素开发原则的机制。基于目标状态,Knative configuration控制器为应用创建了一个新的Kubernetes部署应用。并且configuration的变更会体现在一个新的Kubernetes部署应用中。
修订版(Revision):Knative configuration遵循12要素开发原则,每次应用的变更将会创建一个新的Knative Revision。Revision类似于版本控制中的标签。Revision一旦创建,是不可改变的。每个Revision都有一个对应的Kubernetes Deployment。它允许将应用程序回滚到任何正确的最新配置。
路由(Route):Knative Route是访问Knative Service的URL。

接下来分别以java和node为例创建一个简单的web服务。该服务接收到HTTP GET请求时,会根据环境变量Target传递的内容向Response输出Hello$TATGET!内容。

1.1、制作service镜像

java

  1. 创建一个spring boot项目
package com.example.helloworld;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
public class HelloworldApplication {@Value("${TARGET:World}")String target;@RestControllerclass HelloworldController {@GetMapping("/")String hello() {return "Hello " + target + "!";}}public static void main(String[] args) {SpringApplication.run(HelloworldApplication.class, args);}
}

启动访问

./mvnw package && java -jar target/helloworld-0.0.1-SNAPSHOT.jar
  1. 制作Dockerfile文件
# Use the official maven/Java 8 image to create a build artifact.
# https://hub.docker.com/_/maven
FROM maven:3.5-jdk-8-alpine as builder
# Copy local code to the container image.
WORKDIR /app
COPY pom.xml .
COPY src ./src
# Build a release artifact.
RUN mvn package -DskipTests
# Use AdoptOpenJDK for base image.
# It's important to use OpenJDK 8u191 or above that has container support enabled.
# https://hub.docker.com/r/adoptopenjdk/openjdk8
# https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds
FROM adoptopenjdk/openjdk8:jdk8u202-b08-alpine-slim
# Copy the jar to the production image from the builder stage.
COPY --from=builder /app/target/helloworld-*.jar /helloworld.jar
# Run the web service on container startup.
CMD ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/helloworld.jar"]
  1. 制作镜像、并push到Docker registry
# Build the container on your local machine
docker build -t {username}/helloworld-java-spring .
# Push the container to docker registry
docker push {username}/helloworld-java-spring

nodejs

  1. 创建一个Node项目
npm init
package name: (helloworld-nodejs)
version: (1.0.0)
description:
entry point: (index.js)
test command:
git repository:
keywords:
author:
license: (ISC) Apache-2.0
  1. 安装express包
npm install express
  1. 创建一个主程序index.js
const express = require('express');
const app = express();
app.get('/', (req, res) => {console.log('Hello world received a request.');const target = process.env.TARGET || 'World';res.send(`Hello ${target}!\n`);
});
const port = process.env.PORT || 8080;
app.listen(port, () => {console.log('Hello world listening on port', port);
});
  1. 配置package.json
{"name": "knative-serving-helloworld","version": "1.0.0","description": "Simple hello world sample in Node","main": "index.js","scripts": {"start": "node index.js"},"author": "","license": "Apache-2.0","dependencies": {"express": "^4.16.4"}
}
  1. 制作Dockerfile文件
# Use the official lightweight Node.js 12 image.
# https://hub.docker.com/_/node
FROM node:12-slim
# Create and change to the app directory.
WORKDIR /usr/src/app
# Copy application dependency manifests to the container image.
# A wildcard is used to ensure both package.json AND package-lock.json are copied.
# Copying this separately prevents re-running npm install on every code change.
COPY package*.json ./
# Install production dependencies.
RUN npm install --only=production
# Copy local code to the container image.
COPY . ./
# Run the web service on container startup.
CMD [ "npm", "start" ]

制作镜像和推送镜像和java环境一样。
注意应用程序和docker暴露端口必须是8080,否则会部署失败。新版可以在service的yml中指定端口号。

1.2 部署应用

可以采用二种方式部署应用:

  • yaml
  • kn
  1. yaml方式

创建service.yml,image指定为上一步制作的镜像,文件如下:

apiVersion: serving.knative.dev/v1
kind: Service
metadata:name: helloworld-java-springnamespace: default
spec:template:spec:containers:- image: docker.io/{username}/helloworld-java-springenv:- name: TARGETvalue: "Java Spring Sample v1"

利用kubectl部署

kubectl apply --filename service.yaml
  1. kn部署
kn service create helloworld-java-spring --image=docker.io/{username}/helloworld-java-spring --env TARGET="Java Spring Sample v1"

在创建服务期间,Knative 会执行以下步骤:

  • 为此版本的应用程序创建一个新的不可变修订版。
  • 为您的应用程序创建路由、入口、服务和负载平衡。
  • 自动向上和向下扩展您的 pod,包括缩小到零活动 pod。

1.3 验证

可以采用二种方式验证:

  • kubectl
  • kn
  1. kubectl
kubectl get ksvc helloworld-java-spring  --output=custom-columns=NAME:.metadata.name,URL:.status.url

显示

NAME                      URL
helloworld-java-spring    http://helloworld-java-spring.default.1.2.3.4.sslip.io
  1. kn
kn service describe helloworld-java-spring -o url

显示

http://helloworld-java-spring.default.1.2.3.4.sslip.io

访问:

curl http://helloworld-java-spring.default.1.2.3.4.sslip.io
Hello Java Spring Sample v1!
# Even easier with kn:
curl $(kn service describe helloworld-java-spring -o url)

1.4 删除service

  1. kubectl方式
kubectl delete --filename service.yaml
  1. kn方式
kn service delete helloworld-java-spring

1.4 更新Knative Service configuration

在部署完一个Knative Service后,我们会因为应用版本的升级、配置的变更等需要更新现有服务的configuration。Knative服务还提供了一种机制实现回滚变更。
12要素应用设计原则规定,应用程序与配置的变更应被视为一个新的修订版。修订版是不可变更的应用和配置的状态集合。它可以让你回滚到任何一个有效的修订版状态。
应用的更新包括容器镜像的更新、健康检查探针的调整、环境变量的变更。这些变更会导致Knative生成新的修订版。每一个新修订版将创建一个新的Kubernetes Deployment对象。
接下来,我们通过一个更新服务配置的示例来演示配置的变更。

apiVersion: serving.knative.dev/v1
kind: Service
metadata:name: helloworld-go  # Service名称namespace: default
spec:template:metadata:name: helloworld-go-v2 # Knative Revision名称spec:containers:- image: cnlab/helloworld-goenv:- name: TARGETvalue: "Go Sample v2"livenessProbe:httpGet:path: /readinessProbe:httpGet:path: /

配置文件(service.yaml)的变更如下。
1)更新修订版的名称(.spec.template.metadata.name)为helloworld-go-v2,区别于上一个修订版名称helloworld-go-v1。
2)更新环境变量TARFET(.spec.template.spec.containers[0].env[0].value)的值为Go Sample v2。
将配置更新到Knative:

# kubectl apply-f service.yaml

检查部署结果:

# kubectl get ksvc helloworld-go
NAME            URL                    LATESTCREATED        LATESTREADY       READY  REASON
helloworld-go   http://helloworld-go.  helloworld-go-v2     helloworld-go-v2  Truedefault.example.com

通过curl命令访问helloworld-go服务:

##获取集群任一节点的IP地址和nodePort端口
# IP_ADDRESS="$(kubectl get nodes-o 'jsonpath={.items[0].status.addresses[0].address}'):$(kubectl get svc istio-ingressgateway--namespace istio-system --output 'jsonpath={.spec.ports[?(@.port==80)].nodePort}')"# curl-H "Host:helloworld-go.default.example.com" http://$IP_ADDRESS
Hello Go Sample v2!

查看部署后生成的Kubernetes Deployment:

# kubectl get deployments
NAME                          READY   UP-TO-DATE   AVAILABLE   AGE
helloworld-go-v1-deployment   0/0     0            0           2m52s
helloworld-go-v2-deployment   1/1     1            1           2m19s

查看部署后生成的Kubernetes Pod:

# kubectl get pods
NAME                                          READY   STATUS    RESTARTS   AGE
helloworld-go-v2-deployment-589c5f7ff9-czpj2   3/3     Running   0          12s

helloworld-go对应的部署如果在扩缩容时间窗口期(默认60s)内没有请求,Knative将自动将对应的部署缩容为零。
查看部署后生成的Revision:

# kubectl get revision
NAME               CONFIG NAME    K8S SERVICE NAME   GENERATION   READY   REASON
helloworld-go-v1  helloworld-go   helloworld-go-v1  1            True
helloworld-go-v2  helloworld-go   helloworld-go-v2  2            True

我们可以看到helloworld-go的配置有两个修订版,分别是helloworld-go-v1和helloworld-go-v2。配置的变更产生了新的修订版,然而并没有产生新的路由、服务和配置对象。我们可以通过下面的命令来验证这些资源对象的状态。

  • 获取服务的路由信息的命令:kubectl get routes。
  • 获取Knative服务的状态信息的命令:kubectl get ksvc。
  • 获取Knative服务的配置信息的命令:kubectl get configurations。
  • Knative默认路由策略是将所有流量转发给最新的修订版。

1.5 流量分发到不同版本

在典型的微服务部署中,实现流量在不同版本中分发是实现金丝雀或蓝绿部署方式的基础。Knative提供了这种流量分发方式的支持。
在Knative Service的yaml文件配置中,traffic区块描述了如何在多个版本之间进行流量分发。配置范例如下:

apiVersion: serving.knative.dev/v1
kind: Service
metadata:name: helloworld-go  # Service名称namespace: default
spec:template:metadata:name: helloworld-go-v2 # Knative Revision名称spec:containers:- image: cnlab/helloworld-goenv:- name: TARGETvalue: "Go Sample v2"livenessProbe:httpGet:path: /readinessProbe:httpGet:path: /traffic:- tag: v1  revisionName: helloworld-go-v1  # Revision的名称percent: 50  #流量切分的百分比的数字值- tag: v2revisionName: helloworld-go-v2  # Revision的名称percent: 50  #流量切分的百分比的数字值

traffic区块中可以有一个或多个条目。每个条目中带有如下属性。

  • tag:流量分配的标识符。此标记将在路由中充当前缀,以便将流量分发到特定修订版。
  • revisionName:参与流量分配的Knative服务修订版本的名称。
  • percent:对应修订版被分配的流量百分比。这个值在0~100之间。在上述例子中,Knative分配给修订版helloworld-go-v1和helloworld-go-v2各50%的流量。
  • Knative Serving会为每个Tag创建独特的URL。我们可以通过下面的命令查看:
# kubectl get ksvc helloworld-go-o jsonpath='{.status.traffic[*].url}'
http://v1-helloworld-go.default.example.com
http://v2-helloworld-go.default.example.com

通过访问URL可以直接访问到对应的修订版。

1.6 蓝绿部署与灰度发布

一般情况下,升级服务端应用需要先停掉老版本服务,再启动新版服务。但是这种简单的发布方式存在两个问题,一方面在新版本升级过程中,服务是暂时中断的;另一方面,如果新版本升级失败,回滚起来非常麻烦,容易造成更长时间的服务不可用。

  1. 蓝绿部署

所谓蓝绿部署,是指同时运行两个版本的应用,即部署的时候,并不停掉老版本,而是直接部署一套新版本,等新版本运行起来后,再将流量切换到新版本上,如图6-3所示。但是蓝绿部署要求服务端在升级过程中同时运行两套程序,对硬件资源的要求是日常所需的2倍。

Knative提供了一个简单的切换流量的方法,可将流量快速从Revison1切换到Revision 2。如果Revision2发生错误,服务可以快速回滚变更到Revison1。
接下来,我们将通过helloworld-go这个Knative服务来应用蓝绿色部署模式。上文拥有两个修订版的helloworld-go服务,名称分别为helloworld-go-v1和helloworld-go-v2。通过部署helloworld-go-v2,我们可以看到Knative自动将100%的流量路由到helloworld-go-v2。现在,假设出于某些原因,我们需要将helloworld-go-v2回滚到helloworld-go-v1。
以下示例中Knative Service与先前部署的helloworld-go相同,只是添加了traffic部分以指示将100%的流量路由到helloworld-go-v1。

apiVersion: serving.knative.dev/v1
kind: Service
metadata:name: helloworld-go  # Service名称namespace: default
spec:template:metadata:name: helloworld-go-v2 # Knative Revision名称spec:containers:- image: cnlab/helloworld-goenv:- name: TARGETvalue: "Go Sample v2"livenessProbe:httpGet:path: /readinessProbe:httpGet:path: /traffic:- tag: v1  revisionName: helloworld-go-v1     # Revision的名称percent: 100                 # 流量切分的百分比值- tag: v2revisionName: helloworld-go-v2     # Revision的名称percent: 0                     # 流量切分的百分比值- tag: latest                     # 默认最新的RevisionlatestRevision: truepercent: 0                     # 关闭默认流量分配
  1. 灰度发布

灰度发布也叫金丝雀发布。如图所示,在灰度发布开始后,先启动一个新版本应用,但是并不直接将流量切过来,而是测试人员对新版本进行线上测试。启动的这个新版本应用,就是我们的金丝雀。如果测试没有问题,我们可以将少量的流量导入新版本,然后再对新版本做运行状态观察,收集各种运行时数据。如果此时对新旧版本做数据对比,就是所谓的A/B测试。

当确认新版本运行良好后,再逐步将更多的流量导入新版本。在此期间,我们还可以不断地调整新旧两个版本运行的服务器副本数量,使得新版本能够承受更大的流量压力,直到将100%的流量切换到新版本上,最后关闭剩下的老版本服务,完成灰度发布。
如果我们在灰度发布过程中(灰度期)发现新版本有问题,应该立即将流量切回老版本,这样就会将负面影响控制在最小范围内。
以下示例通过不断变更helloworld-go-v1和helloworld-go-v2的流量比例来实现helloworld-go服务新版本的灰度发布。

apiVersion: serving.knative.dev/v1
kind: Service
metadata:name: helloworld-go               # Service名称namespace: default
spec:template:metadata:name: helloworld-go-v2          # Knative Revision名称spec:containers:- image: cnlab/helloworld-goenv:- name: TARGETvalue: "Go Sample v2"livenessProbe:httpGet:path: /readinessProbe:httpGet:path: /traffic:- tag: v1  revisionName: helloworld-go-v1     # Revision的名称percent: 80                    # 流量切分的百分比值- tag: v2revisionName: helloworld-go-v2     # Revision的名称percent: 20                    # 流量切分的百分比值- tag: latest                    # 默认最新的RevisionlatestRevision: truepercent: 0                    # 关闭默认流量分配

使用 Knative CLI 路由和管理流量

1.5和1.6的操作也可以通过cli命令设置。

kn service update <service-name> --traffic <revision-name>=<percent>

是您为其配置流量路由的 Knative 服务的名称。
是要配置为接收一定百分比的流量的修订名称。
是要发送到 指定的修订版的流量百分比。

例如,要拆分名为 example 的 Service 的流量,将 80% 的流量发送到 Revision 绿色,将 20% 的流量发送到 Revision blue,您可以运行以下命令:

kn service update example-service --traffic green=80 --traffic blue=20

也可以将标签添加到修订版,然后根据您设置的标签拆分流量:

kn service update example --tag green=revision-0001 --tag blue=@latest

@latest 标签表示蓝色解析为服务的最新版本。 以下示例将 80% 的流量发送到最新版本,将 20% 的流量发送到名为 v1 的版本。

kn service update example-service --traffic @latest=80 --traffic v1=20

1.7 Knative Service的弹性伸缩配置

无服务器计算不仅能够终止未使用的服务,还可以按需扩展计算规模。Knative Serving支持这种弹性伸缩能力。
为了让Knative的Autoscaler更好地调度服务,我们需要根据实际情况在服务中添加相应的扩缩容配置项。下面以helloworld-go.yaml范例来演示扩缩容相关配置。

apiVersion: serving.knative.dev/v1
kind: Service
metadata:name: helloworld-go  # Service名称namespace: default
spec:template:metadata:annotations:autoscaling.knative.dev/class: "kpa.autoscaling.knative.dev" # Autoscaler的实现方式,可选值有"kpa.autoscaling.knative.dev" 或 "hpa.autoscaling.knative.dev"autoscaling.knative.dev/metric: "concurrency"  # 设置度量指标为Concurrency(默认值),还可以根据业务情况选择RPS或CPUautoscaling.knative.dev/target: "10"  # 设置单个Pod最大并发数为10,默认值为100autoscaling.knative.dev/minScale: "1"   # minScale表示最小保留实例数为1autoscaling.knative.dev/maxScale: "100" # maxScale表示最大扩容实例数为3spec:containerConcurrency: 10                  # 并发请求数的硬性限制containers:- image: cnlab/helloworld-go

在上述配置中,revision中配置了修订版的弹性伸缩策略。各个属性代表的意义如下。

  • autoscaling.knative.dev/class:表示Autoscaler的实现方式,这个属性的可选值有kpa.autoscaling.knative.dev或hpa.autoscaling.knative.dev。KPA支持缩容到零,HPA支持基于CPU的扩展机制。
  • autoscaling.knative.dev/metric:度量指标默认为并发数,该属性还可以根据业务情况选择每秒请求数或CPU使用率。
  • autoscaling.knative.dev/target:自动缩放的目标值是Autoscaler维护应用的每个副本度量指标的目标值。
  • autoscaling.knative.dev/minScale:表示每个修订版副本需要保留的最小数量。在任何时间点,副本不会少于这个数量。通过该设置,我们可以有效地减少服务的冷启动时间。
  • autoscaling.knative.dev/maxScale:表示每个修订版副本所能达到的最大数量。在任何时间点,副本都不会超过指定的最大值,从而避免资源被过度使用。
  • containerConcurrency:限制容器在给定时间允许的并发请求的数量的硬性限制。只有当应用程序需要强制的并发约束时,才会使用到该属性。

部署helloworld-go服务并配置到Knative集群:

# kubectl apply-f helloworld-go.yaml

验证部署结果:

#IP_ADDRESS="$(kubectl get nodes-o 'jsonpath={.items[0].status.addresses[0].address}'):$(kubectl get svc istio-ingressgateway--namespace istio-system--output 'jsonpath={.spec.ports[?(@.port==80)].nodePort}')"
# curl-H "Host:helloworld-go.default.example.com" $IP_ADDRESS
Hello World!

压力测试:

# hey-c 50-z 30s-host "helloworld-go.default.knative.k8s.arch.dapp.com" "http://$IP_ADDRESS"

通过hey工具发起50个并发请求,持续30秒对hellowrold-go服务进行压测。
查看压测期间Pod的副本数量:

# kubectl get pod -l serving.knative.dev/service=helloworld-go
NAME                                             READY   STATUS    RESTARTS   AGE
helloworld-go-7t7sg-deployment-6bfbdb84fd-5l5gc  3/3     Running   0          42s
helloworld-go-7t7sg-deployment-6bfbdb84fd-99cdr  3/3     Running   0          42s
helloworld-go-7t7sg-deployment-6bfbdb84fd-ls4ks  3/3     Running   0          44s
helloworld-go-7t7sg-deployment-6bfbdb84fd-n4s4k  3/3     Running   0          44s
helloworld-go-7t7sg-deployment-6bfbdb84fd-q9kr8  3/3     Running   0          40s
helloworld-go-7t7sg-deployment-6bfbdb84fd-r77tt  3/3     Running   0          22m

通过上面的命令,我们可以看到集群中产生了6个Pod副本。那么问题来了,我们发起的并发请求数是50个,服务自动缩放的目标值是10,按照“副本数=并发数/目标值”算法,Pod副本数应该是5个才对呀。这是由于Knative Serving还有一个控制参数叫目标使用率,一旦并发数达到预设目标的70%(默认值),Autoscaler就会继续扩容。引入目标使用率的主要目的是在扩容时减小由Pod启动时间带来的延迟,使负载到达前就将Pod实例启动起来。

引用

Hello world apps - Python - 《Knative v0.23 Documentation》 - 书栈网 · BookStack

Knative实战:基于Kubernetes的无服务器架构实践 by 李志伟 游杨 (z-lib.org)

serverless knative实战相关推荐

  1. Serverless Knative Serving弹性扩缩容实践整理

    文章目录 (一)基础 (1)认识 (2)Knative Serving对象模型 (3)knative-serving (4)Knative的扩缩容流程原理 (二)弹性扩缩容实践 (1)自动扩缩容类型选 ...

  2. Knative 实战:三步走!基于 Knative Serverless 技术实现一个短网址服务

    短网址顾名思义就是使用比较短的网址代替很长的网址.维基百科上面的解释是这样的: 短网址又称网址缩短.缩短网址.URL 缩短等,指的是一种互联网上的技术与服务,此服务可以提供一个非常短小的 URL 以代 ...

  3. Knative 实战:基于 Knative Serverless 技术实现天气服务-下篇

    上一期我们介绍了如何基于 Knative Serverless 技术实现天气服务-上篇,首先我们先来回顾一下上篇介绍的内容: 通过高德天气 API 接口,每隔 3 个小时定时发送定时事件,将国内城市未 ...

  4. 从零入门 Serverless | Knative 带来的极致 Serverless 体验

    作者 | 冬岛 阿里巴巴高级技术专家 Serverless 公众号后台回复 "knative",即可免费下载<Knative 云原生应用开发指南>电子书! **导读:* ...

  5. Knative 实战:一个微服务应用的部署

    作者 | 元毅 阿里云智能事业群高级开发工程师 在 Istio 中提供了一个 Bookinfo 的示例,用于演示微服务之间的调用,那么如何在 Knative 中部署这个示例呢?本文将会给大家介绍一下在 ...

  6. Knative 实战:如何在 Knative 中配置自定义域名及路由规则

    作者 | 元毅 阿里云智能事业群高级开发工程师 当前 Knative 中默认支持是基于域名的转发,可以通过域名模板配置后缀,但目前对于用户来说并不能指定全域名设置.另外一个问题就是基于 Path 和 ...

  7. Knative 实战:基于 Kafka 实现消息推送

    作者 | 元毅 阿里云智能事业群高级开发工程师 导读:当前在 Knative 中已经提供了对 Kafka 事件源的支持,那么如何基于 Kafka 实现消息推送呢?本文作者将以阿里云 Kafka 产品为 ...

  8. Knative 实战:基于阿里云 Kafka 实现消息推送

    在 Knative 中已经提供了对 Kafka 事件源的支持,那么如何在阿里云上基于 Kafka 实现消息推送,本文给大家解锁这一新的姿势. 背景 消息队列 for Apache Kafka 是阿里云 ...

  9. React Firebase 计账软件 Serverless 项目实战视频教程

    学完本课程,您将掌握: React 基础 创建 React 项目 组件 useState useEffect React Hook 发送请求 React Router Firebase Authent ...

最新文章

  1. maven-assembly-plugin和maven-shade-plugin打包区别及弊端
  2. php控制字数方法,php处理字数过多的方法
  3. java接口自动化Excel占位符_基于maven+java+TestNG+httpclient+poi+jsonpath+ExtentReport的接口自动化测试框架...
  4. Java——集合转数组并对其进行遍历
  5. 庆祝51CTO六周年:资源牛人有奖比拼,生日当天疯狂送豆!(已结束)
  6. C语言逐行读取文件内容
  7. 熬夜整理了免费的数据源网站大全,再也不愁找不到数据练手
  8. 在WINCE中的一些VB.NET2005通用方法
  9. 二叉树常见算法总结(java)
  10. 删除Windows下的Linux系统
  11. cordova打包之android应用签名
  12. 项目启动报 JDBC Driver has been forcibly unregistered
  13. Choerodon猪齿鱼实践之集群管理(一)
  14. html解压zip文件怎么打开方式,zip文件是什么文件,如何打开zip格式的文件?
  15. 【Houdini MAYA】从MAYA到Houdini入门学习笔记(三)
  16. 遗传算法入门(连载之十) 神经网络入门(连载预告)
  17. 浅谈深度学习落地问题
  18. 图文推荐系统之数据冷启小结
  19. 算法转 Java 后端,2021秋招斩获腾讯、京东、百度等大厂 offer 面经分享!
  20. 买个ssl证书费用要多少钱?

热门文章

  1. 删除SVN图标及文件
  2. RabbitMQ 通俗易懂 简单开发(一)
  3. JS下载文件到本地集合
  4. openjudge 1.7.7 配对碱基链
  5. 工业控制系统安全控制基线
  6. 机械波简述------说说纵波(声波是一种纵波)
  7. java分页插件valuelist
  8. 人在江湖飘,哪能不挨刀。CENTOS之后,UBUNTU,FEDORA都要安装起来作测试啊
  9. ES搜索--轻量搜索语法
  10. 字符串某个字符修改颜色、给字符串添加