开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

深度揭秘Netty中的FastThreadLocal为什么比

发表于 2021-11-23

「这是我参与11月更文挑战的第23天,活动详情查看:2021最后一次更文挑战」

file

阅读这篇文章之前,建议先阅读和这篇文章关联的内容。

1. 详细剖析分布式微服务架构下网络通信的底层实现原理(图解)

2. (年薪60W的技巧)工作了5年,你真的理解Netty以及为什么要用吗?(深度干货)

3. 深度解析Netty中的核心组件(图解+实例)

4. BAT面试必问细节:关于Netty中的ByteBuf详解

5. 通过大量实战案例分解Netty中是如何解决拆包黏包问题的?

6. 基于Netty实现自定义消息通信协议(协议设计及解析应用实战)

7. 全网最详细最齐全的序列化技术及深度解析与应用实战

8. 手把手教你基于Netty实现一个基础的RPC框架(通俗易懂)

9. (年薪60W分水岭)基于Netty手写实现RPC框架进阶篇(带注册中心和注解)

FastThreadLocal的实现与J.U.C包中的ThreadLocal非常类似。

了解过ThreadLocal原理的同学应该都清楚,它有几个关键的对象.

  1. Thread
  2. ThreadLocalMap
  3. ThreadLocal

同样,Netty专门为FastThreadLocal量身打造了FastThreadLocalThread和InternalThreadLocalMap两个重要的类。下面我们看下这两个类是如何实现的。

PS,如果不懂ThreadLocal的朋友,可以看我这篇文章:ThreadLocal的使用及原理分析

FastThreadLocalThread是对Thread类的一层包装,每个线程对应一个InternalThreadLocalMap实例。只有FastThreadLocal和FastThreadLocalThread组合使用时,才能发挥 FastThreadLocal的性能优势。首先看下FastThreadLocalThread的源码定义:

1
2
3
4
5
java复制代码public class FastThreadLocalThread extends Thread {

private InternalThreadLocalMap threadLocalMap;
// 省略其他代码
}

可以看出 FastThreadLocalThread 主要扩展了 InternalThreadLocalMap 字段,我们可以猜测到 FastThreadLocalThread 主要使用 InternalThreadLocalMap 存储数据,而不再是使用 Thread 中的 ThreadLocalMap。所以想知道 FastThreadLocalThread 高性能的奥秘,必须要了解 InternalThreadLocalMap 的设计原理。

InternalThreadLocalMap

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
java复制代码public final class InternalThreadLocalMap extends UnpaddedInternalThreadLocalMap {

private static final int DEFAULT_ARRAY_LIST_INITIAL_CAPACITY = 8;

private static final int STRING_BUILDER_INITIAL_SIZE;

private static final int STRING_BUILDER_MAX_SIZE;

public static final Object UNSET = new Object();

private BitSet cleanerFlags;
private InternalThreadLocalMap() {
indexedVariables = newIndexedVariableTable();
}
private static Object[] newIndexedVariableTable() {
Object[] array = new Object[INDEXED_VARIABLE_TABLE_INITIAL_SIZE];
Arrays.fill(array, UNSET);
return array;
}
public static int lastVariableIndex() {
return nextIndex.get() - 1;
}

public static int nextVariableIndex() {
int index = nextIndex.getAndIncrement();
if (index < 0) {
nextIndex.decrementAndGet();
throw new IllegalStateException("too many thread-local indexed variables");
}
return index;
}
// 省略

}

从 InternalThreadLocalMap 内部实现来看,与 ThreadLocalMap 一样都是采用数组的存储方式。

了解ThreadLocal的同学都知道,它内部也是采用数组的方式来实现hash表,对于hash冲突,采用了线性探索的方式来实现。

但是 InternalThreadLocalMap 并没有使用线性探测法来解决 Hash 冲突,而是在 FastThreadLocal 初始化的时候分配一个数组索引 index,index 的值采用原子类 AtomicInteger 保证顺序递增,通过调用 InternalThreadLocalMap.nextVariableIndex() 方法获得。然后在读写数据的时候通过数组下标 index 直接定位到 FastThreadLocal 的位置,时间复杂度为 O(1)。如果数组下标递增到非常大,那么数组也会比较大,所以 FastThreadLocal 是通过空间换时间的思想提升读写性能。

下面通过一幅图描述 InternalThreadLocalMap、index 和 FastThreadLocal 之间的关系。

image-20211123112056607

通过上面 FastThreadLocal 的内部结构图,我们对比下与 ThreadLocal 有哪些区别呢?

FastThreadLocal 使用 Object 数组替代了 Entry 数组,Object[0] 存储的是一个Set<FastThreadLocal<?>> 集合。

从数组下标 1 开始都是直接存储的 value 数据,不再采用 ThreadLocal 的键值对形式进行存储。

假设现在我们有一批数据需要添加到数组中,分别为 value1、value2、value3、value4,对应的 FastThreadLocal 在初始化的时候生成的数组索引分别为 1、2、3、4。如下图所示。

image-20211123112505405

至此,我们已经对 FastThreadLocal 有了一个基本的认识,下面我们结合具体的源码分析 FastThreadLocal 的实现原理。

FastThreadLocal的set方法源码分析

在讲解源码之前,我们回过头看下上文中的 ThreadLocal 示例,如果把示例中 ThreadLocal 替换成 FastThread,应当如何使用呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public class FastThreadLocalTest {

private static final FastThreadLocal<String> THREAD_NAME_LOCAL = new FastThreadLocal<>();
private static final FastThreadLocal<TradeOrder> TRADE_THREAD_LOCAL = new FastThreadLocal<>();
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
int tradeId = i;
String threadName = "thread-" + i;
new FastThreadLocalThread(() -> {
THREAD_NAME_LOCAL.set(threadName);
TradeOrder tradeOrder = new TradeOrder(tradeId, tradeId % 2 == 0 ? "已支付" : "未支付");
TRADE_THREAD_LOCAL.set(tradeOrder);
System.out.println("threadName: " + THREAD_NAME_LOCAL.get());
System.out.println("tradeOrder info:" + TRADE_THREAD_LOCAL.get());
}, threadName).start();

}
}
}

可以看出,FastThreadLocal 的使用方法几乎和 ThreadLocal 保持一致,只需要把代码中 Thread、ThreadLocal 替换为 FastThreadLocalThread 和 FastThreadLocal 即可,Netty 在易用性方面做得相当棒。下面我们重点对示例中用得到 FastThreadLocal.set()/get() 方法做深入分析。

首先看下 FastThreadLocal.set() 的源码:

1
2
3
4
5
6
7
8
java复制代码public final void set(V value) {
if (value != InternalThreadLocalMap.UNSET) {
InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
setKnownNotUnset(threadLocalMap, value);
} else {
remove();
}
}

FastThreadLocal.set() 方法实现并不难理解,先抓住代码主干,一步步进行拆解分析。set() 的过程主要分为三步:

  1. 判断 value 是否为缺省值,如果等于缺省值,那么直接调用 remove() 方法。这里我们还不知道缺省值和 remove() 之间的联系是什么,我们暂且把 remove() 放在最后分析。
  2. 如果 value 不等于缺省值,接下来会获取当前线程的 InternalThreadLocalMap。
  3. 然后将 InternalThreadLocalMap 中对应数据替换为新的 value。

InternalThreadLocalMap.get()

先来看InternalThreadLocalMap.get()方法:

1
2
3
4
5
6
7
8
java复制代码public static InternalThreadLocalMap get() {
Thread thread = Thread.currentThread();
if (thread instanceof FastThreadLocalThread) {
return fastGet((FastThreadLocalThread) thread);
} else {
return slowGet();
}
}

如果thread实例类型是FastThreadLocalThread,则调用fastGet()。

InternalThreadLocalMap.get() 逻辑很简单.

  1. 如果当前线程是 FastThreadLocalThread 类型,那么直接通过 fastGet() 方法获取 FastThreadLocalThread 的 threadLocalMap 属性即可
  2. 如果此时 InternalThreadLocalMap 不存在,直接创建一个返回。

关于 InternalThreadLocalMap 的初始化在上文中已经介绍过,它会初始化一个长度为 32 的 Object 数组,数组中填充着 32 个缺省对象 UNSET 的引用。

1
2
3
4
5
6
7
java复制代码private static InternalThreadLocalMap fastGet(FastThreadLocalThread thread) {
InternalThreadLocalMap threadLocalMap = thread.threadLocalMap();
if (threadLocalMap == null) {
thread.setThreadLocalMap(threadLocalMap = new InternalThreadLocalMap());
}
return threadLocalMap;
}

否则,则调用slowGet(),从代码实现来看,slowGet() 是针对非 FastThreadLocalThread 类型的线程发起调用时的一种兜底方案。如果当前线程不是 FastThreadLocalThread,内部是没有 InternalThreadLocalMap 属性的,Netty 在 UnpaddedInternalThreadLocalMap 中保存了一个 JDK 原生的 ThreadLocal,ThreadLocal 中存放着 InternalThreadLocalMap,此时获取 InternalThreadLocalMap 就退化成 JDK 原生的 ThreadLocal 获取。

1
2
3
4
5
6
7
8
java复制代码private static InternalThreadLocalMap slowGet() {
InternalThreadLocalMap ret = slowThreadLocalMap.get();
if (ret == null) {
ret = new InternalThreadLocalMap();
slowThreadLocalMap.set(ret);
}
return ret;
}

setKnownNotUnset

获取 InternalThreadLocalMap 的过程已经讲完了,下面看下 setKnownNotUnset() 如何将数据添加到 InternalThreadLocalMap 的。

1
2
3
4
5
java复制代码private void setKnownNotUnset(InternalThreadLocalMap threadLocalMap, V value) {
if (threadLocalMap.setIndexedVariable(index, value)) {
addToVariablesToRemove(threadLocalMap, this);
}
}

setKnownNotUnset() 主要做了两件事:

  1. 找到数组下标 index 位置,设置新的 value。
  2. 将 FastThreadLocal 对象保存到待清理的 Set 中。

首先我们看下第一步 threadLocalMap.setIndexedVariable() 的源码实现:

1
2
3
4
5
6
7
8
9
10
11
java复制代码public boolean setIndexedVariable(int index, Object value) {
Object[] lookup = indexedVariables;
if (index < lookup.length) {
Object oldValue = lookup[index];
lookup[index] = value;
return oldValue == UNSET;
} else {
expandIndexedVariableTableAndSet(index, value);
return true;
}
}

indexedVariables 就是 InternalThreadLocalMap 中用于存放数据的数组,如果数组容量大于 FastThreadLocal 的 index 索引,那么直接找到数组下标 index 位置将新 value 设置进去,事件复杂度为 O(1)。在设置新的 value 之前,会将之前 index 位置的元素取出,如果旧的元素还是 UNSET 缺省对象,那么返回成功。

如果数组容量不够了怎么办呢?InternalThreadLocalMap 会自动扩容,然后再设置 value。接下来看看 expandIndexedVariableTableAndSet() 的扩容逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码private void expandIndexedVariableTableAndSet(int index, Object value) {
Object[] oldArray = indexedVariables;
final int oldCapacity = oldArray.length;
int newCapacity = index;
newCapacity |= newCapacity >>> 1;
newCapacity |= newCapacity >>> 2;
newCapacity |= newCapacity >>> 4;
newCapacity |= newCapacity >>> 8;
newCapacity |= newCapacity >>> 16;
newCapacity ++;

Object[] newArray = Arrays.copyOf(oldArray, newCapacity);
Arrays.fill(newArray, oldCapacity, newArray.length, UNSET);
newArray[index] = value;
indexedVariables = newArray;
}

可以看出 InternalThreadLocalMap 实现数组扩容几乎和 HashMap 完全是一模一样的,所以多读源码还是可以给我们很多启发的。InternalThreadLocalMap 以 index 为基准进行扩容,将数组扩容后的容量向上取整为 2 的次幂。然后将原数组内容拷贝到新的数组中,空余部分填充缺省对象 UNSET,最终把新数组赋值给 indexedVariables。

思考关于基准扩容

思考:为什么 InternalThreadLocalMap 以 index 为基准进行扩容,而不是原数组长度呢?

假设现在初始化了 70 个 FastThreadLocal,但是这些 FastThreadLocal 从来没有调用过 set() 方法,此时数组还是默认长度 32。当第 index = 70 的 FastThreadLocal 调用 set() 方法时,如果按原数组容量 32 进行扩容 2 倍后,还是无法填充 index = 70 的数据。所以使用 index 为基准进行扩容可以解决这个问题,但是如果 FastThreadLocal 特别多,数组的长度也是非常大的。

回到 setKnownNotUnset() 的主流程,向 InternalThreadLocalMap 添加完数据之后,接下就是将 FastThreadLocal 对象保存到待清理的 Set 中。我们继续看下 addToVariablesToRemove() 是如何实现的:

addToVariablesToRemove

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码private static void addToVariablesToRemove(InternalThreadLocalMap threadLocalMap, FastThreadLocal<?> variable) {
Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);
Set<FastThreadLocal<?>> variablesToRemove;
if (v == InternalThreadLocalMap.UNSET || v == null) {
variablesToRemove = Collections.newSetFromMap(new IdentityHashMap<FastThreadLocal<?>, Boolean>());
threadLocalMap.setIndexedVariable(variablesToRemoveIndex, variablesToRemove);
} else {
variablesToRemove = (Set<FastThreadLocal<?>>) v;
}

variablesToRemove.add(variable);
}

variablesToRemoveIndex 是采用 static final 修饰的变量,在 FastThreadLocal 初始化时 variablesToRemoveIndex 被赋值为 0。InternalThreadLocalMap 首先会找到数组下标为 0 的元素.

  1. 如果该元素是缺省对象 UNSET 或者不存在,那么会创建一个 FastThreadLocal 类型的 Set 集合,然后把 Set 集合填充到数组下标 0 的位置。
  2. 如果数组第一个元素不是缺省对象 UNSET,说明 Set 集合已经被填充,直接强转获得 Set 集合即可。这就解释了 InternalThreadLocalMap 的 value 数据为什么是从下标为 1 的位置开始存储了,因为 0 的位置已经被 Set 集合占用了。

思考关于Set集合设计

思考:为什么 InternalThreadLocalMap 要在数组下标为 0 的位置存放一个 FastThreadLocal 类型的 Set 集合呢?这时候我们回过头看下 remove() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public final void remove(InternalThreadLocalMap threadLocalMap) {
if (threadLocalMap == null) {
return;
}

Object v = threadLocalMap.removeIndexedVariable(index);
removeFromVariablesToRemove(threadLocalMap, this);

if (v != InternalThreadLocalMap.UNSET) {
try {
onRemoval((V) v);
} catch (Exception e) {
PlatformDependent.throwException(e);
}
}
}

在执行 remove 操作之前,会调用 InternalThreadLocalMap.getIfSet() 获取当前 InternalThreadLocalMap。

有了之前的基础,理解 getIfSet() 方法就非常简单了。

  1. 如果是 FastThreadLocalThread 类型,直接取 FastThreadLocalThread 中 threadLocalMap 属性。
  2. 如果是普通线程 Thread,从 ThreadLocal 类型的 slowThreadLocalMap 中获取。

找到 InternalThreadLocalMap 之后,InternalThreadLocalMap 会从数组中定位到下标 index 位置的元素,并将 index 位置的元素覆盖为缺省对象 UNSET。

接下来就需要清理当前的 FastThreadLocal 对象,此时 Set 集合就派上了用场,InternalThreadLocalMap 会取出数组下标 0 位置的 Set 集合,然后删除当前 FastThreadLocal。最后 onRemoval() 方法起到什么作用呢?Netty 只是留了一处扩展,并没有实现,用户需要在删除的时候做一些后置操作,可以继承 FastThreadLocal 实现该方法。

FastThreadLocal.get()源码分析

再来看一下 FastThreadLocal.get() 的源码:

1
2
3
4
5
6
7
8
9
java复制代码public final V get() {
InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
Object v = threadLocalMap.indexedVariable(index);
if (v != InternalThreadLocalMap.UNSET) {
return (V) v;
}

return initialize(threadLocalMap);
}

首先根据当前线程是否是 FastThreadLocalThread 类型找到 InternalThreadLocalMap,然后取出从数组下标 index 的元素,如果 index 位置的元素不是缺省对象 UNSET,说明该位置已经填充过数据,直接取出返回即可。

1
2
3
4
java复制代码public Object indexedVariable(int index) {
Object[] lookup = indexedVariables;
return index < lookup.length? lookup[index] : UNSET;
}

如果 index 位置的元素是缺省对象 UNSET,那么需要执行初始化操作。可以看到,initialize() 方法会调用用户重写的 initialValue 方法构造需要存储的对象数据.

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码private V initialize(InternalThreadLocalMap threadLocalMap) {
V v = null;
try {
v = initialValue();
} catch (Exception e) {
PlatformDependent.throwException(e);
}

threadLocalMap.setIndexedVariable(index, v);
addToVariablesToRemove(threadLocalMap, this);
return v;
}

initialValue方法的构造方式如下。

1
2
3
4
5
6
java复制代码private final FastThreadLocal<String> threadLocal = new FastThreadLocal<String>() {
@Override
protected String initialValue() {
return "hello world";
}
};

构造完用户对象数据之后,接下来就会将它填充到数组 index 的位置,然后再把当前 FastThreadLocal 对象保存到待清理的 Set 中。整个过程我们在分析 FastThreadLocal.set() 时都已经介绍过,就不再赘述了。

到此为止,FastThreadLocal 最核心的两个方法 set()/get() 我们已经分析完了。下面有两个问题我们再深入思考下。

  1. FastThreadLocal 真的一定比 ThreadLocal 快吗?答案是不一定的,只有使用FastThreadLocalThread 类型的线程才会更快,如果是普通线程反而会更慢。
  2. FastThreadLocal 会浪费很大的空间吗?虽然 FastThreadLocal 采用的空间换时间的思路,但是在 FastThreadLocal 设计之初就认为不会存在特别多的 FastThreadLocal 对象,而且在数据中没有使用的元素只是存放了同一个缺省对象的引用,并不会占用太多内存空间。

版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Mic带你学架构!
如果本篇文章对您有帮助,还请帮忙点个关注和赞,您的坚持是我不断创作的动力。欢迎关注同名微信公众号获取更多技术干货!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

聚合支付设计方案,该如何设计?

发表于 2021-11-23

一、项目目标

支付中心架构将各业务的公共交易、支付、财务等沉淀到支付中心,并主要解决了以下三个主要问题:

  1. 建立基础订单、支付、财务统一体系,抽象和封装公共处理逻辑,形成统一的基础服务,降低业务的接入成本及重复研发成本;
  2. 构建安全、稳定、可扩展的系统,为业务的快速发展和创新需求提供基础支撑,解决业务「快」和支付「稳」之间的矛盾;
  3. 沉淀核心交易数据,同时为应用端、物业公司、用户提供数据支撑。

二、具体调用流程

在目标的指导下,我向集采、o2o、收费易三个项目组的相关开发咨询了业务逻辑,再结合我们自己的业务场景调整了支付中心调用流程和两个注意点

首先我们来看一下支付中心的调用过程。业务系统、支付中心和第三方通道的交互流程图如下:

图片

各系统交互流程为:

  1. 物业公司开通第三方支付渠道商户,并获取第三方支付参数
  2. 物业公司将第三方支付参数提供给支付中心,开通商户号,开通支付渠道,获取商户标识和支付标识。
  3. 物业公司将商户标识和支付标识提供给应用端。至此,物业公司注册流程完毕。接下来是支付流程。
  4. 应用端使用物业公司提供的商户标识和支付标识,以及必备的支付订单号,支付金额,调起方式,上送至支付中心。
  5. 支付中心将获取的标识解析到对应的参数,并整合应用端的请求参数,向第三方支付发起支付,并获取支付发起的结果。
  6. 支付中心将发起结果整合后直接返回给应用端,注意,这里只是这个请求是否发起成功的通知,并不是最终支付结果的通知。
  7. 第三方支付调起用户的支付或者跳转收银台页面、小程序调起用户支付进行支付,第三方支付获取到用户的支付结果之后。回调通知支付中心。
  8. 支付中心处理数据,并回调通知应用端。
  9. 应用端处理订单信息,并开始订单、通知用户。

注意:

1.订单号问题,问题起因:有些应用系统,使用订单号上传,有些使用自己系统中的流水号上传并发起支付。所以这里设计如下:

  • 应用系统上送的无论是订单号还是流水号,支付中心都不直接使用,而是进行记录,并重新生成一个唯一的流水号,上送第三方支付。
  • 第三方支付会在校验参数成功确认支付发起成功后,再返回由第三方支付生成的流水号,用于以后的账单查询,对账,退款等功能。
  • 支付中心会保存三个流水、订单号。方便以后调用、查询。
  • 在收到第三方支付的调用返回时,支付中心会重组调用返回参数,将应用上送的订单号,支付中心生成的唯一流水号,第三方支付返回的流水号,一并返回应用端,建议应用端都进行保留。

2.这里还涉及到退款使用哪个号进行退款的问题,这里设计为:使用支付中心流水号判定使用哪一笔订单退款。上送了支付中心生成的流水号后,根据流水号和商户标识以及支付标识检索出来的结果,进行退款,退款金额不可超过该笔流水号支付的金额。应用端可以根据业务需求自行选择退款方式,支付中心只做和流水号相关的退款。

3.有关收银台,现在有些第三方支付存在自己的收银台,有的没有,所以支付中心必须有自己的收银台,但同时如果第三方支付存在已有收银台也没有必要跳转两次。所以这里的逻辑设计为:如果第三方存在必须跳转的收银台,使用第三方收银台,其余情况直接使用支付中心收银台。

三、支付中心架构设计

目前的系统功能整体架构如下:

图片

如图所示,从架构上主要分为四个大模块:

  1. 支付中心后台:主要是账号管理相关,物业公司的开户开通支付等提供支持
  2. 支付消息:主要是用于对应用端进行通知
  3. 交易核心:用来支撑整个系统的基础交易核心,参数组装发起,返回数据的处理,异常的处理和通知等。
  4. 渠道网关:解析应用端发送过来的请求,证书白名单的设置和使用,第三方api的调用等

支付中心后台:

收银台:

图片

渠道网关

(1)支付账户管理

图片

物业公司选择自己所需的支付渠道进行开通

用户选择自己倾向的支付方式

最后请求中由支付中心处理,收入对应的收款账户。

(2)request解析器

图片

一个请求在进入request解析器之后,首先解析支付标识,决定使用哪个支付插件(alipayPlugin, wechatPlugin, easyPlugin)

其次解析调起方式(小程序,PC,APP)获取可用的支付插件(alipaypaymentappexecutor,xxxexecutor)

最后选择方法(onpay waponpay refund)

交易核心:

交易核心的数据库设计:

图片

分账资金流向:

图片

四、目前预见的可能的问题

1,数据监控

出现数据异常,或者报错,及时在钉钉群里通知。

2,数据一致性问题

咱们的系统打算暂时只做一个模块,应用端可以到支付中心来同步数据。

3,稳定性问题,第三方支付不够稳定

主要是用户可能会用微信支付失败,又用支付宝支付。

这个需要应用端进行监控,支付中心对于提供的不同订单号会实时发起支付。同一订单号,连续发起两次之间间隔不超过15秒。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【python】 字典 与 集合 的特点,及它们背后的散列表

发表于 2021-11-23

「这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战」

字典的特点

  • 键查询很快:字典类型提供了无视数据量大小的快速访问——只要字典能被装在内存里。
  • 当往 dict 里添加新键而又发生散列冲突的时候,新键可能会被安排存放到另一个位置。但字典是否等价则与键的添加次序无关。
  • 往字典里添加新键可能会改变已有键的顺序:无论何时往字典里添加新的键,Python 解释器都可能做出为字典扩容的决定。扩容导致的结果就是要新建一个更大的散列表,并把字典里已有的元素添加到新表里。这个过程中可能会发生新的散列冲突,导致新散列表中键的次序变化。
    • 因此不要对字典同时进行迭代和修改。如果想扫描并修改一个字典,最好分成两步来进行:
      1. 首先对字典迭代,以得出需要添加的内容,把这些内容放在一个新字典里;
      2. 迭代结束之后再对原有字典进行更新。
  • 由于字典使用了散列表,而散列表又必须是稀疏的,这导致它在空间上的效率低下。如果需要存放数量巨大的记录,那么放在由元组或是具名元组构成的列表中会是比较好的选择。
    • 在用户自定义的类型中,__slots__ 属性可以改变实例属性的存储方式,由 dict 变成 tuple 。

集合的特点

  • 集合里的元素必须是可散列的。
  • 集合很消耗内存。
  • 可以很高效地判断元素是否存在于某个集合。
  • 元素的次序取决于被添加到集合里的次序。
  • 往集合里添加元素,可能会改变集合里已有元素的次序。

dict 和 set 背后:散列表

  • 在具有一千万个浮点数的字典中使用 in 查找一个数花费约三分之一微秒(0.000000337s)。使用列表需要 0.01s。
  • Python 会设法保证大概还有三分之一的表元是空的,所以在快要达到这个阈值的时候,原有的散列表会被复制到一个更大的空间里面。
  • 内置的 hash() 方法可以用于所有的内置类型对象。如果是自定义对象调用 hash() 的话,实际上运行的是自定义的 __hash__ 。
  • 如果两个对象在比较的时候是相等的,那它们的散列值必须相等。例如,如果 1 == 1.0 为真,那么 hash(1) == hash(1.0) 也必须为真。
  • 从 Python 3.3 开始,str 、bytes 和 datetime 对象的散列值计算过程中多了随机的“加盐”这一步。所加盐值是 Python 进程内的一个常量,但是每次启动 Python 解释器都会生成一个不同的盐值。随机盐值的加入是为了防止 DOS 攻击而采取的 一种安全措施。
  • 为了获取 my_dict[search_key] 背后的值,Python 首先会调用 hash(search_key) 来计算 search_key 的散列值,把这个值最低的几位数字当作偏移量,在散列表里查找表元(具体取几位,得看当前散列表的大小)。若找到的表元是空的,则抛出 KeyError 异 常。若不是空的,则表元里会有一对 found_key:found_value 。 这时候 Python 会检验 search_key == found_key 是否为真,如果它们相等的话,就会返回 found_value 。若发生散列冲突,则会在散列值取另一部分,来得散列表中的另一行。
  • 一个可散列的对象必须满足以下要求:
    1. 支持 hash() 函数,并且通过 __hash__() 方法所得到的散列值是不变的。
    2. 支持通过 __eq__() 方法来检测相等性。
    3. 若 a == b 为真,则 hash(a) == hash(b) 也为真。 所有由用户自定义的对象默认都是可散列的,因为它们的散列值由 id() 来获取,而且它们都是不相等的。
  • 如果实现了一个类的 __eq__ 方法,并且希望它是可散列的,那么它一定要有个恰当的 __hash__ 方法,保证在 a == b 为真的情况下 hash(a) == hash(b) 也必定为真。另一方面,如果一个含有自定义的 __eq__ 依赖的类处于可变的状态,那就不要在这个类中实现 __hash__方法,因为它的实例是不可散列的。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

python爬虫-基金信息存储

发表于 2021-11-23

这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战

星舰人类的家不是地球,不要返航,这里不是家!你们的家在远方!

1 前言

前面已经讲了很多次要进行数据存储,终于在上一篇中完成了数据库的设计,在这一篇就开始数据的存储操作,在数据存储的这个部分,会将之前抓取到的基金列表,基金基本信息和基金变动信息以及ETF信息进行存储。

2 信息存储

2.1 基金基本信息存储

在这里获取基金信息包括两个部分,一部分是场外基金另外一部分是场外基金信息。之在前的文章中,我们已经获完成了场外基金和ETF基金信息的代码,因此在这里我们仅仅需要存储数据库即可,那么有个问题,基金的信息是随时发生变更的或者更新的。在保存时需要先判断基金代码是否已经存在,如果存在则更新,如果不存在则新增,但是这样效率有点低,这时候就用到之前的文章内容,使用这样 on duplicate key update 的语句就可以一条sql搞定了。举例如下所示:

1
2
go复制代码INSERT INTO `tb_fund_list`(`code`, `name`, `fund_type`) VALUES ('000363','国泰聚信价值优势混合C','混合型-灵活')
on duplicate key update `code` = '000363', `name` = '国泰聚信价值优势混合C' ,`fund_type` = '混合型-灵活';

如果存在000363基金的话,我们就进行更新操作,如果不存在那么久插入数据。具体实现的代码如下图所示:

2.2 基金变动信息获取

基金的变动信息不论是场内基金还是场外基金都是一样的获取方式,在这里就可以使用通用的逻辑进行处理了,就是之前抓取基金变动信息和基金价格信息的方式。

3 需要改进的地方
3.1 基金类型

现在基金基本信息中的基金类型还是中文,这样的中文存储起来不符合常用的编码规范,之前没有处理是因为还不知道有多少种基金的类型,现在已经获取到了所有的基金,这个时候我们需要查询一下所有的基金类型,然后建立枚举来表述不同的基金类型。

1
2
csharp复制代码# 获取所有的基金类型信息
select distinct fund_type from tb_fund_list;

根据查询出来的基金类型,最终定义的基金类型如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
makefile复制代码fund_type_dic = {
"QDII": "1",
"商品(不含QDII)": "2",
"股票型": "3",
"指数型-股票": "4",
"混合型-偏债": "51",
"混合型-偏股": "52",
"混合型-平衡": "53",
"混合型-灵活": "61",
"债券型-中短债": "62",
"债券型-可转债": "63",
"债券型-混合债": "64",
"债券型-长债": "65"
}

根据经验来说,债券型的基金相对比较多,如果对债券基金感兴趣的可以时常更新数据,在后续的操作中以非债券型基金为主进行分析,数据总量相对较小,批量更新的时间也相对较短。

3.2 基金的更新顺序

在之前的基金获取过程中,总体来讲获取的顺序是混乱的,在最终的数据结果存储时,需要将获取的信息基金拼接和组装。最终的更新数据顺序为:

  • 1 更新场外基金列表(新增或者更新)
  • 2 更新ETF信息列表(新增或者更新)
  • 3 查询基金的基本信息进行更新操作
  • 4 查询基金的阶段变动信息进行更新

4 总结

至此,获取基金的信息已经完毕,已经把基金信息保存成功,在下一章中将介绍如何建立线性模型去评估基金的分数,为投资基金做出定量分析。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

C语言指针

发表于 2021-11-23

这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战

一、指针的概念

1.1变量和地址

  • 变量:直观来说,int a、char ch、float num这些都是声明变量,而a、ch、num就是变量
  • 地址:在计算机中,内存被分为一小块一小块的,而每一块都有一个编号,叫做地址。
  • 一般变量都存储在内存当中。而每块内存都有一个独一无二的地址,这个地址就是指针
  • 如果把内存比作一个宾馆,在声明一个变量时(int a),就相当于在宾馆前台办了入住手续。前台会给你一个门卡和门牌号,简单理解门牌号就是地址。

二、变量的指针与指针变量

指针为变量的地址,而专门用来存储另一个变量的地址的变量就是指针变量。

2.1、指针变量的定义及使用

(1)、指针变量的定义

定义指针变量的符合为*,如下定义了三个指针变量。它们的变量名为pi、pj、pf,而不是*pi、*pj、*pf。*号在此只用来声明。

1
2
3
4
5
6
7
arduino复制代码//声明了两个整型变量和一个浮点型变量
int i, j;
float f;

//声明三个指针变量
int *pi, *pj;
float *pf;

(2)、指针变量的使用

  • 取地址符&:单目运算符 “&” 的功能是取操作对象的地址。常量、表达式和寄存器变量不能取地址,因为它们不是存放在内存某个存储单元中,而是放在寄存器中,寄存器无地址。
  • 指针运算符(间接寻址运算符):单目运算符 **“”\\ 的功能是按照操作对象的地址值,访问对应存储单元。与“&”互为逆运算。
1
2
3
4
5
6
7
8
9
10
11
perl复制代码//声明一个变量i,初始化值为10
int i = 10;

//利用取地址符&获取i的地址
printf("%d", &i);

//定义一个指针变量pi,指向i的地址
int *pi = &i;

//利用指针运算符*获取pi指向的内存,即为i的值
printf("%d", *pi);

注:在C语言中,所有变量的声明都必须放在最前面,但是有些编译器你没放前面也可以通过,这里注意一下

(3)、&和*运算符的结合方向

“&”和“*”两个运算符优先级相同,但按从右至左方向结合。可理解为从右开始运算

1
2
3
4
5
6
arduino复制代码//声明一个变量i
int i = 10;
//声明一个指针变量pi,指向i
int *pi = &i;
//输出i的地址
printf("%d", &*pi);

上面的代码定义了一个指向i的指针变量pi,而输出i的地址使用了“&pi”。首先,pi是一个指针变量,pi的内容为i的地址。因为运算符是右结合,则先是运算pi。即为pi地址中的内容,就是10。然后再取地址,&*pi即为i的地址。

2.2、指针变量的初始化

1
2
3
4
5
csharp复制代码void main(){
int a = 10;
//利用取地址符&,获取变量a的地址,给指针变量pa赋值
int *pa = &a;
}

2.3、指针运算

(1)赋值运算

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码void main(){
int *px, *py, *pz, x;
//1、指向某个地址
px = &x;

//2、赋予空指针
py = NULL;

//3、赋予指定地址
pz = 4000;

}

(2)指针与整数的加减运算

  • 指针变量自增或自减,即指针向前或者向后移动一个存储单元
  • 指针比那里加上一个整型数,即指针向前或者向后移动指定的存储单元

(3)关系运算

  • px < py,判断px指向的地址是否小于py指向的地址
  • px == py,判断px和py是否指向同一个地址
  • px == 0和px != 0表示px是否为空指针

接下来来一个小练习:

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
arduino复制代码//声明函数
void invert(int *a, int start, int end);

/**
* 采用递归法对a数组的元素进行逆序
*/
void main(){

}

/**
* 实现函数
* a为数组首地址
* i位起始逆序元素
* j为逆序结尾元素
*/
void invert(int *a, int start, int end){
//临时变量,用于交换
int temp;

//当起始逆序元素小于逆序结尾元素时,说明还没有逆序到中间元素
if(start < end){
//将起始元素和结尾元素交换
temp = a[start];
a[start] = a[end];
a[end] = temp;

//交换后再次调用invert,将其余元素逆序,此时start和end要同时向中间移动
invert(a, start + 1, end - 1);
}
}

三、指针与数组

3.1、指向数组的指针

  • 数组名即为该数组的首地址,a为一个数组,a = &a[0]。
  • 可以通过指针对数组元素进行访问,a = a[0]、(a + 1) = a[1]。
  • 数组名不能进行指针的操作,像指针p++是合法的,但是数组a++是非法的。

3.2、字符指针和字符数组

在C语言中,系统本身没有提供字符串数据类型,但可以使用两种方式存储一个字符串:字符数组方式和字符指针方式。

(1)字符数组方式

也就是我们比较常用的方式

1
2
3
4
5
arduino复制代码void main(){
//定义一个字符数组
char sentence[] = "Do not go gengle into that good night!";
printf("%s", sentence);
}

其中sentence就是字符数组的首地址。

(2)字符指针方式实现字符串

1
2
3
4
arduino复制代码void main(){
char *sentence = "Do not go gentle into that good night!";
printf("%s", sentence);
}

来个小练习:

1
2
3
4
5
6
7
8
9
10
11
12
13
scss复制代码/**
* 用数组将字符串sentence复制到字符串copy
*/
void mian(){
char *sentence = "Do not go gentle into that good night!", copy[50];
int i;
//当没有遇到结束符时,一直循环
for(i = 0; sentence[i] != '\0'; i++){
//将数据复制到copy中
copy[i] = sentence[i];
}
printf("复制后的copy是:%s", copy);
}

3.3、多级指针及指针数组

(1)多级指针

简单来说就是指针的指针,指针变量作为一个变量,也有自己的存储空间。而这个存储空间也有一个地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
perl复制代码void main(){
//定义一个普通变量
int a = 10;

//定义一个指针变量,指向a
int *p = &a;

//定义另一个指针变量,指向指针变量p,此时pp就是二级指针
int **pp = &p;

//输出两个指针
printf("一级指针pa为:%d\n", p);
printf("二级指针ppa为:%d", pp);

//指针的指针和普通指针操作一样,可以用*pp获取pp指向地址中的内容,即p存储的内容
printf("p存储的内容为:%d", *pp);
}

注:因为一级指针和二级指针性质不一样,所以一级指针和二级指针之间不能赋值,如p = pp在编译时会报错(这是书中写的,但是在我实际测试当中,可以赋值,可能是编译器的问题)。

(2)指针数组

即一个元素为指针的数组,定义如下:

1
css复制代码int *a[10];

用一个练习熟悉指针数组,解释全在注释当中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
css复制代码void main(){
//定义并初始化一个int数组
int a[5] = {1, 3, 5, 6, 8}, i;

//定义一个指针数组,与a数组中元素对应
int *p[5];
for(i = 0; i < 5; i++){
p[i] = &a[i];
}

//定义一个二级指针,存放指针数组的首地址。指针数组和普通数组一样,数组名为数组首地址
int **pp = p;

//利用指针数组首地址输出数据
for(i = 0; i < 5; i++){

//数组a中第零个元素地址为p,而p的的地址为pp,所以**pp = a[0]
//数组a中第一个元素地址为p + 1,而p + 1的地址为pp + 1,所有**(pp + 1) = a[1]
//以此类推,**(pp + n) = a[n]
printf("%d\t", **(pp + i));
}
}

3.4、指针与多维数组

(1)多维数组的地址

假设有个二维数组a[4][2],那么可以分两个维度来理解这个数组。

  • 先去掉[2],只看“a[4]”一个维度。此时a只是个普通的一维数组,而后面的[2]也只是决定了数组a元素的性质
  • 在数组a中,有四个元素,我们取一个来分析第二个维度。其第一个元素为a[0],我们将a[0]看做一个整体,不作为数组元素,只作为一个名称X。那么第二个维度就可以看做X[2],即一个有两个元素的数组。
  • 由上面可知,X数组的首地址为数组名,即X。X实际上是a[0],类推的话X1、X2等就是a[1]、a[2]。可以间接理解为数组的第一个维度装的全是地址,每个元素X的地址。

​

(2)多维数组的指针

举个例子方便理解

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
ini复制代码void mian(){
//创建一个普通二维数组
int num[5][5] = {
{1, 3, 4, 5, 6},
{4, 5, 7, 8, 8},
{6, 8, 9, 0, 1},
{3, 4, 2, 1, 2},
{4, 5, 6, 3, 2}
};

//声明一个指针数组
int *p_num[5];
int i, j;

//初始化指针数组,每个元素分别指向num[0][0]、num[1][0]、、、
for(i = 0; i < 5; i++){
p_num[i] = &num[i][0];
}

//利用指针数组p_num输出num数组中的元素
for(i = 0; i < 5; i++){

printf("这是第%d轮数组\n", i+1);
for(j = 0; j < 5; j++){

//将p_num[i]作为一个数组首地址,数组存储的内容为*(p_num+j)
printf("%d\t", *(p_num[i] + j));
}
printf("\n");
}


}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Kubernetes 入门教程 前言 一 k8s架构 二 搭

发表于 2021-11-23

简介:本文是一篇 kubernetes(下文用 k8s 代替)的入门文章,将会涉及 k8s 的架构、集群搭建、一个 Redis 的例子,以及如何使用 operator-sdk 开发 operator 的教程。在文章过程中,会穿插引出 Pod、Deployment、StatefulSet 等 k8s 的概念,这些概念通过例子引出来,更容易理解和实践。

作者 | 凡澈

来源 | 阿里技术公众号

前言

本文是一篇 kubernetes(下文用 k8s 代替)的入门文章,将会涉及 k8s 的架构、集群搭建、一个 Redis 的例子,以及如何使用 operator-sdk 开发 operator 的教程。在文章过程中,会穿插引出 Pod、Deployment、StatefulSet 等 k8s 的概念,这些概念通过例子引出来,更容易理解和实践。文章参考了很多博客以及资料,放在最后参考资料部分。

一 k8s架构

我们看下 k8s 集群的架构,从左到右,分为两部分,第一部分是 Master 节点(也就是图中的 Control Plane),第二部分是 Node 节点。

Master 节点一般包括四个组件,apiserver、scheduler、controller-manager、etcd,他们分别的作用是什么:

  • Apiserver:上知天文下知地理,上连其余组件,下接ETCD,提供各类 api 处理、鉴权,和 Node 上的 kubelet 通信等,只有 apiserver 会连接 ETCD。
  • Controller-manager:控制各类 controller,通过控制器模式,致力于将当前状态转变为期望的状态。
  • Scheduler:调度,打分,分配资源。
  • Etcd:整个集群的数据库,也可以不部署在 Master 节点,单独搭建。

Node 节点一般也包括三个组件,docker,kube-proxy,kubelet

  • Docker:具体跑应用的载体。
  • Kube-proxy:主要负责网络的打通,早期利用 iptables,现在使用 ipvs技术。
  • Kubelet:agent,负责管理容器的生命周期。

总结一下就是 k8s 集群是一个由两部分组件 Master 和 Node 节点组成的架构,其中 Master 节点是整个集群的大脑,Node 节点来运行 Master 节点调度的应用,我们后续会以一个具体的调度例子来解释这些组件的交互过程。

二 搭建 k8s 集群

上面说完了 k8s 集群中有哪些组件,接下来我们先看下如何搭建一个 k8s 集群,有以下几种方法(参考文末链接):

  • 当我们安装了 Docker Desktop APP 之后,勾选 k8s 支持就能搭建起来。
  • 使用 MiniKube 来搭建,社区提供的一键安装脚本。
  • 直接在云平台购买,例如阿里云 ack。
  • 使用 kubeadmin,这是 k8s 社区推荐的可以部署生产级别 k8s 的工具。
  • 使用二进制,下载各组件安装,此教程需要注意,下载的各组件版本要和博客中保持一致,就可以成功。

本文后面的例子均采用本地 Docker Desktop APP 搭建的 k8s。

1
2
3
css复制代码➜  ~ kubectl version
Client Version: version.Info{Major:"1", Minor:"21", GitVersion:"v1.21.4", GitCommit:"3cce4a82b44f032d0cd1a1790e6d2f5a55d20aae", GitTreeState:"clean", BuildDate:"2021-08-11T18:16:05Z", GoVersion:"go1.16.7", Compiler:"gc", Platform:"darwin/amd64"}
Server Version: version.Info{Major:"1", Minor:"21", GitVersion:"v1.21.4", GitCommit:"3cce4a82b44f032d0cd1a1790e6d2f5a55d20aae", GitTreeState:"clean", BuildDate:"2021-08-11T18:10:22Z", GoVersion:"go1.16.7", Compiler:"gc", Platform:"linux/amd64"}

三 从需求出发

下面我们从一个实际的需求出发,来看看如何在 k8s 上部署 Redis 服务。

  • 部署一个Redis服务
  • 支持高可用
  • 提供统一的 EndPoint 访问地址

1 部署单机版

如果我们想在 k8s 上部署一个单机版本 Redis,我们执行下面的命令即可:

1
2
3
4
5
arduino复制代码➜  ~ kubectl run redis --image=redis
pod/redis created
➜ ~ kubectl get pods
NAME READY STATUS RESTARTS AGE
redis 1/1 Running 0 5s

可以用 kubectl exec 来进入到 Pod 内部连接 Redis 执行命令:

1
2
3
4
5
ruby复制代码➜  ~ kubectl exec -it redis -- bash
root@redis:/data# redis-cli
127.0.0.1:6379> ping
PONG
127.0.0.1:6379>

那么 Pod 和 Redis 是什么关系呢?这里的 Redis 其实是一个 Docker 进程启动的服务,但是在 k8s 中,它叫 Pod。

2 Pod 与 Deployment

我们来讲下第一个 k8s 的概念 Pod,Pod 是 k8s 中最小的调度单元,一个 Pod 中可以包含多个 Docker,这些 Docker 都会被调度到同一台 Node 上,这些 Docker 共享 NetWork Namespace,并且可以声明共享同一个 Volume 来共享磁盘空间。

这样的好处是什么呢?其实在真实的世界中,很多应用是有部署在同一台机器的需求的,比如 Redis 日志采集插件要采集日志,肯定需要和 Redis 部署在同一台机器上才能读到 Redis 的日志,我们前面讲述背景的时候说到了 Docker Swarm 存在一些问题,其中之一就是它只是基于 Docker 调度,虽然也可以设置亲和度让两台 Docker 调度在同一个机器上,但是因为不能一起调度,所以会存在一个Docker 提前被调度到了一个资源少的机器上,从而导致第二个 Docker 调度失败。

例如我们一共有 2 台容器,A和B,分别为 Redis 和 日志采集组件,各需要 2g 内存,现在有两台 node,node1 3.5 内存,node2 4g内存,在 Docker Swarm 的调度策略下,先调度 Redis,有可能被调度到了 node1 上,接下来再来调度日志采集组件,发现 node1 只有 1.5g 内存了,调度失败。但是在 k8s 中,调度是按照 pod 来调度的,两个组件在一个 pod 中,调度就不会考虑 node1。

虽然 Pod 已经可以运行 Redis 服务了,但是他不具备高可用性,因为一旦一个 Pod 与一个节点(Node)绑定,除非这个绑定发生了变化(pod.spec.node 字段被修改),否则它永远都不会离开这个节点,这也就意味着,如果这个宿主机宕机了,这个 Pod 也不会主动迁移到其他节点上去。为了让服务可以一直在,需要使用 Deployment 这样的控制器。

1
2
3
4
5
6
7
sql复制代码➜  ~ kubectl create deployment redis-deployment --image=redis
deployment.apps/redis-deployment created
➜ ~ kubectl get pods
NAME READY STATUS RESTARTS AGE
redis 1/1 Running 0 32m
redis-deployment-866c4c6cf9-8z8k5 1/1 Running 0 8s
➜ ~

redis-deployment-866c4c6cf9-8z8k5就是刚才通过 kubectl create 创建的新的 Deployment,为了验证高可用,我们把用 kubectl delete pod 把 redis 和 redis-deployment-866c4c6cf9-8z8k5都删掉看会发生什么。

1
2
3
4
5
6
7
sql复制代码➜  ~ kubectl delete pod redis redis-deployment-866c4c6cf9-8z8k5
pod "redis" deleted
pod "redis-deployment-866c4c6cf9-8z8k5" deleted
➜ ~ kubectl get pods
NAME READY STATUS RESTARTS AGE
redis-deployment-866c4c6cf9-zskkb 1/1 Running 0 10s
➜ ~

redis已经消失了,但是redis-deployment-866c4c6cf9-zskkb换了个名字又出现了!

Deployment 可以定义多副本个 Pod,从而为应用提供迁移能力,如果单纯使用 Pod,实际上当应用被调度到某台机器之后,机器宕机应用也无法自动迁移,但是使用 Deployment,则会调用 ReplicaSet(一种控制器) 来保证当前集群中的应用副本数和指定的一致。

3 k8s 使用 yaml 来描述命令

k8s 中,可以使用 kubectl 来创建简单的服务,但是还有一种方式是对应创建复杂的服务的,就是提供 yaml 文件。例如上面的创建 Pod 的命令,我们可以用下面的 yaml 文件替换,执行 kubectl create 之后,可以看到 redis Pod 又被创建了出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
yaml复制代码➜  ~ cat pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: redis
spec:
containers:
- name: redis
image: redis
➜ ~ kubectl create -f pod.yaml
pod/redis created
➜ ~ kubectl get pods
NAME READY STATUS RESTARTS AGE
redis 1/1 Running 0 6s
redis-deployment-866c4c6cf9-zskkb 1/1 Running 0 6m32s

四 k8s 组件调用流程

下面我们看下kubectl create deployment redis-deployment –image=redis下发之后,k8s 集群做了什么。

  • 首先 controller-manager, scheduler, kubelet 都会和 apiserver 开始进行 List-Watch 模型,List 是拿到当前的状态,Watch 是拿到期望状态,然后 k8s 集群会致力于将当前状态达到达期望状态。
  • kubectl 下发命令到 apiserver,鉴权处理之后将创建信息存入 etcd,Deployment 的实现是使用 ReplicaSet 控制器,当 controller-manager 提前拿到当前的状态(pod=0),接着接收到期望状态,需要创建 ReplicaSet(pod=1),就会开始创建 Pod。
  • 然后 scheduler 会进行调度,确认 Pod 被创建在哪一台 Node 上。
  • 之后 Node 上的 kubelet 真正拉起一个 docker。

这些步骤中,apiserver 的作用是不言而喻的,所以说上接其余组件,下连 ETCD,但是 apiserver 是可以横向扩容的,然后通过负载均衡,倒是 ETCD 在 k8s 架构中成了瓶颈。

最开始看这架构的时候,会想着为啥 apiserver, scheduler, controller-manager 不合成一个组件,其实在 Google Borg 中,borgmaster 就是这样的,功能也是这些功能,但是合在了一起,最后他们也发现集群大了之后 borgmaster 会有些性能上的问题,包括 kubelet 的心跳就是很大一块,所以 k8s 从一开始开源,设计中有三个组件也是更好维护代码吧。

五 部署主从版本

上面我们已经部署了 Redis 的单机版,并通过 Deployment 实现了服务持续运行,接下来来看下主从版本如何部署,其中一个比较困难的地方就是如何确定主从的同步关系。

1 StatefulSet

k8s 为有状态应用设计了 StatefulSet 这种控制器,它主要通过下面两个特性来服务有状态应用:

  • 拓扑状态:实例的创建顺序和编号是顺序的,会按照 name-index 来编号,比如 redis-0,redis-1 等。
  • 存储状态:可以通过声明使用外部存储,例如云盘等,将数据保存,从而 Pod 重启,重新调度等都能读到云盘中的数据。

下面我们看下 Redis 的 StatefulSet 的例子:

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
yaml复制代码apiVersion: apps/v1
kind: StatefulSet # 类型为 statefulset
metadata:
name: redis-sfs # app 名称
spec:
serviceName: redis-sfs # 这里的 service 下面解释
replicas: 2 # 定义了两个副本
selector:
matchLabels:
app: redis-sfs
template:
metadata:
labels:
app: redis-sfs
spec:
containers:
- name: redis-sfs
image: redis # 镜像版本
command:
- bash
- "-c"
- |
set -ex
ordinal=`hostname | awk -F '-' '{print $NF}'` # 使用 hostname 获取序列
if [[ $ordinal -eq 0 ]]; then # 如果是 0,作为主
echo > /tmp/redis.conf
else
echo "slaveof redis-sfs-0.redis-sfs 6379" > /tmp/redis.conf # 如果是 1,作为备
fi
redis-server /tmp/redis.conf

接着启动这个 StatefulSet,发现出现了 redis-sfs-0 和 redis-sfs-1 两个 pod,他们正式按照 name-index 的规则来编号的

1
2
3
4
5
6
7
8
sql复制代码➜  ~ kubectl create -f server.yaml
statefulset.apps/redis-sfs created
➜ ~ kubectl get pods
NAME READY STATUS RESTARTS AGE
redis 1/1 Running 0 65m
redis-deployment-866c4c6cf9-zskkb 1/1 Running 0 71m
redis-sfs-0 1/1 Running 0 33s # 按照
redis-sfs-1 1/1 Running 0 28s

接着我们继续看下主从关系生效了没,查看 redis-sfs-1 的日志,却发现:

1
2
3
4
yaml复制代码➜  ~ kubectl logs -f redis-sfs-1
1:S 05 Nov 2021 08:02:44.243 * Connecting to MASTER redis-sfs-0.redis-sfs:6379
1:S 05 Nov 2021 08:02:50.287 # Unable to connect to MASTER: Resource temporarily unavailable
...

2 Headless Service

似乎 redis-sfs-1 不认识 redis-sfs-0,原因就在于我们还没有让它们互相认识,这个互相认识需要使用 k8s 一个服务叫 Headless Service,Service 是 k8s 项目中用来将一组 Pod 暴露给外界访问的一种机制。比如,一个 Deployment 有 3 个 Pod,那么我就可以定义一个 Service。然后,用户只要能访问到这个 Service,它就能访问到某个具体的 Pod,一般有两种方式:

  • VIP:访问 VIP 随机返回一个后端的 Pod
  • DNS:通过 DNS 解析到后端某个 Pod 上

Headless Service 就是通过 DNS 的方式,可以解析到某个 Pod 的地址,这个 DNS 地址的规则就是:

下面我们创建集群对应的 Headless Service:

1
2
3
4
5
6
7
8
9
10
11
12
13
yaml复制代码apiVersion: v1
kind: Service
metadata:
name: redis-sfs
labels:
app: redis-sfs
spec:
clusterIP: None # 这里的 None 就是 Headless 的意思,表示会主动由 k8s 分配
ports:
- port: 6379
name: redis-sfs
selector:
app: redis-sfs

再次查看,发现 redis-sfs-1 已经主备同步成功了,因为创建 Headless Service 之后,redis-sfs-0.redis-sfs.default.svc.cluster.local 在集群中就是唯一可访问的了。

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
yaml复制代码➜  ~ kubectl create -f service.yaml
service/redis-sfs created
➜ ~ kubectl get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 24d
redis-sfs ClusterIP None <none> 6379/TCP 33s
➜ ~ kubectl logs -f redis-sfs-1
...
1:S 05 Nov 2021 08:23:31.341 * Connecting to MASTER redis-sfs-0.redis-sfs:6379
1:S 05 Nov 2021 08:23:31.345 * MASTER <-> REPLICA sync started
1:S 05 Nov 2021 08:23:31.345 * Non blocking connect for SYNC fired the event.
1:S 05 Nov 2021 08:23:31.346 * Master replied to PING, replication can continue...
1:S 05 Nov 2021 08:23:31.346 * Partial resynchronization not possible (no cached master)
1:S 05 Nov 2021 08:23:31.348 * Full resync from master: 29d1c03da6ee2af173b8dffbb85b6ad504ccc28f:0
1:S 05 Nov 2021 08:23:31.425 * MASTER <-> REPLICA sync: receiving 175 bytes from master to disk
1:S 05 Nov 2021 08:23:31.426 * MASTER <-> REPLICA sync: Flushing old data
1:S 05 Nov 2021 08:23:31.426 * MASTER <-> REPLICA sync: Loading DB in memory
1:S 05 Nov 2021 08:23:31.431 * Loading RDB produced by version 6.2.6
1:S 05 Nov 2021 08:23:31.431 * RDB age 0 seconds
1:S 05 Nov 2021 08:23:31.431 * RDB memory usage when created 1.83 Mb
1:S 05 Nov 2021 08:23:31.431 # Done loading RDB, keys loaded: 0, keys expired: 0.
1:S 05 Nov 2021 08:23:31.431 * MASTER <-> REPLICA sync: Finished with success
^C
➜ ~ kubectl exec -it redis-sfs-1 -- bash
root@redis-sfs-1:/data# redis-cli -h redis-sfs-0.redis-sfs.default.svc.cluster.local
redis-sfs-0.redis-sfs.default.svc.cluster.local:6379> ping
PONG
redis-sfs-0.redis-sfs.default.svc.cluster.local:6379>

此时无论我们删除哪个 Pod,它都会按照原来的名称被拉起来,从而可以保证准备关系,这个例子只是一个 StatefulSet 的示例,分析下来可以发现,虽然它可以维护主备关系,但是当主挂了的时候,此时备无法切换上来,因为没有组件可以帮我们做这个切换操作,一个办法是用 Redis Sentinel,可以参考这个项目的配置:k8s-redis-ha-master,如果你的 k8s 较新,需要 merge 此 PR.

六 Operator

虽然有了 StatefulSet,但是这只能对基础版有用,如果想自己定制更加复杂的操作,k8s 的解法是 operator,简而言之,operator 就是定制自己 k8s 对象及对象所对应操作的解法。

那什么是对象呢?一个 Redis 集群,一个 etcd 集群,zk 集群,都可以是一个对象,现实中我们想描述什么,就来定义什么,实际上我们定一个是k8s yaml 中的 kind,之前的例子中,我们使用过 Pod,Deployment,StatefulSet,它们是 k8s 默认实现,现在如果要定义自己的对象,有两个流程:

  • 定义对象,比如你的集群默认有几个节点,都有啥组件
  • 定义对象触发的操作,当创建对象时候要做什么流程,HA 时候要做什么流程等

operator 的方式是基于编程实现的,可以用多种语言,用的最多的就是 go 语言,通常大家会借助 operator-sdk 来完成,因为有很多代码会自动生成。相当于 operator 会生成框架,然后我们实现对应的业务逻辑。

1 准备工作

  • 安装好 go 环境
  • 安装 operator-sdk

2 初始化项目

然后我们按照官网的 sdk 例子,来一步一步实现一个 memcached 的 operator,这里也可以换成 Redis,但是为了保证和官网一致,我们就按照官网来创建 memcached operator。

1
2
3
4
5
6
7
8
9
10
11
12
vbnet复制代码➜  ~ cd $GOPATH/src
➜ src mkdir memcached-operator
➜ src cd memcached-operator
➜ memcached-operator operator-sdk init --domain yangbodong22011 --repo github.com/yangbodong22011/memcached-operator --skip-go-version-check // 这里需要注意 domain 最好是和你在 https://hub.docker.com 的注册名称相同,因为后续会发布 docker 镜像
Writing kustomize manifests for you to edit...
Writing scaffold for you to edit...
Get controller runtime:
$ go get sigs.k8s.io/controller-runtime@v0.9.2
Update dependencies:
$ go mod tidy
Next: define a resource with:
$ operator-sdk create api

3 创建 API 和 Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
vbnet复制代码➜  memcached-operator operator-sdk create api --group cache --version v1alpha1 --kind Memcached --resource --controller
Writing kustomize manifests for you to edit...
Writing scaffold for you to edit...
api/v1alpha1/memcached_types.go
controllers/memcached_controller.go
Update dependencies:
$ go mod tidy
Running make:
$ make generate
go: creating new go.mod: module tmp
Downloading sigs.k8s.io/controller-tools/cmd/controller-gen@v0.6.1
go get: installing executables with 'go get' in module mode is deprecated.
To adjust and download dependencies of the current module, use 'go get -d'.
To install using requirements of the current module, use 'go install'.
To install ignoring the current module, use 'go install' with a version,
like 'go install example.com/cmd@latest'.
For more information, see https://golang.org/doc/go-get-install-deprecation
or run 'go help get' or 'go help install'.
...
go get: added sigs.k8s.io/yaml v1.2.0
/Users/yangbodong/go/src/memcached-operator/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
➜ memcached-operator

上面的步骤实际上生成了一个 operator 的框架,接下来我们首先来定义 memcached 集群都包括啥,将默认实现修改为 Size,表示一个 Memcached 集群中 Memcached 的数量,最后调用 make generate 和 make manifests 来自动生成 deepcopy 和 CRD 资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码➜  memcached-operator vim api/v1alpha1/memcached_types.go // 修改下面 Memcached 集群的定义
// MemcachedSpec defines the desired state of Memcached
type MemcachedSpec struct {
//+kubebuilder:validation:Minimum=0
// Size is the size of the memcached deployment
Size int32 `json:"size"`
}

// MemcachedStatus defines the observed state of Memcached
type MemcachedStatus struct {
// Nodes are the names of the memcached pods
Nodes []string `json:"nodes"`
}

➜ memcached-operator make generate
/Users/yangbodong/go/src/memcached-operator/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
➜ memcached-operator make manifests
/Users/yangbodong/go/src/memcached-operator/bin/controller-gen "crd:trivialVersions=true,preserveUnknownFields=false" rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
➜ memcached-operator

4 实现 Controller

接下来是第二步,定义当创建一个 Memcached 集群时候,具体要干啥。

1
2
3
4
5
6
7
8
vbnet复制代码➜  memcached-operator vim controllers/memcached_controller.go

https://raw.githubusercontent.com/operator-framework/operator-sdk/latest/testdata/go/v3/memcached-operator/controllers/memcached_controller.go //
将 example 换成 yangbodong22011,注意,// 注释中的也要换,实际不是注释,而是一种格式


➜ memcached-operator go mod tidy; make manifests
/Users/yangbodong/go/src/memcached-operator/bin/controller-gen "crd:trivialVersions=true,preserveUnknownFields=false" rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases

5 发布 operator 镜像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
vbnet复制代码➜  memcached-operator vim Makefile
将 -IMG ?= controller:latest 改为 +IMG ?= $(IMAGE_TAG_BASE):$(VERSION)

➜ memcached-operator docker login // 提前登录下 docker
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: yangbodong22011
Password:
WARNING! Your password will be stored unencrypted in /Users/yangbodong/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded
➜ memcached-operator sudo make docker-build docker-push
...
=> => writing image sha256:a7313209e321c84368c5cb7ec820fffcec2d6fcb510219d2b41e3b92a2d5545a 0.0s
=> => naming to docker.io/yangbodong22011/memcached-operator:0.0.1 0.0s
fac03a24e25a: Pushed
6d75f23be3dd: Pushed
0.0.1: digest: sha256:242380214f997d98186df8acb9c13db12f61e8d0f921ed507d7087ca4b67ce59 size: 739

6 修改镜像和部署

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
arduino复制代码➜  memcached-operator vim config/manager/manager.yaml
image: controller:latest 修改为 yangbodong22011/memcached-operator:0.0.1

➜ memcached-operator vim config/default/manager_auth_proxy_patch.yaml
因为国内访问不了 gcr.io
image: gcr.io/kubebuilder/kube-rbac-proxy:v0.8.0 修改为 kubesphere/kube-rbac-proxy:v0.8.0


➜ memcached-operator make deploy
...
configmap/memcached-operator-manager-config created
service/memcached-operator-controller-manager-metrics-service created
deployment.apps/memcached-operator-controller-manager created

➜ memcached-operator kubectl get deployment -n memcached-operator-system // ready 说明 operator 已经部署了
NAME READY UP-TO-DATE AVAILABLE AGE
memcached-operator-controller-manager 1/1 1 1 31s
➜ memcached-operator

7 创建 Memcached 集群

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
vbnet复制代码➜  memcached-operator cat config/samples/cache_v1alpha1_memcached.yaml
apiVersion: cache.yangbodong22011/v1alpha1
kind: Memcached
metadata:
name: memcached-sample
spec:
size: 1
➜ memcached-operator kubectl apply -f config/samples/cache_v1alpha1_memcached.yaml
memcached.cache.yangbodong22011/memcached-sample created
➜ memcached-operator kubectl get pods
NAME READY STATUS RESTARTS AGE
memcached-sample-6c765df685-xhhjc 1/1 Running 0 104s
redis 1/1 Running 0 177m
redis-deployment-866c4c6cf9-zskkb 1/1 Running 0 3h4m
redis-sfs-0 1/1 Running 0 112m
redis-sfs-1 1/1 Running 0 112m
➜ memcached-operator

可以通过 kubectl logs 来查看 operator 的日志:

1
2
arduino复制代码➜  ~ kubectl logs -f deployment/memcached-operator-controller-manager -n memcached-operator-system
2021-11-05T09:50:46.042Z INFO controller-runtime.manager.controller.memcached Creating a new Deployment {"reconciler group": "cache.yangbodong22011", "reconciler kind": "Memcached", "name": "memcached-sample", "namespace": "default", "Deployment.Namespace": "default", "Deployment.Name": "memcached-sample"}

至此,我们的 operator-sdk 的任务暂时告一段落。

七 总结

本文介绍了 k8s 的架构,各组件的功能,以及通过一个循序渐进的 Redis 例子介绍了 k8s 中 Pod, Deployment, StatefulSet 的概念,并通过 operator-sdk 演示了一个完整的 operator制作的例子。

八 参考资料

[1] 《深入剖析Kubernetes》张磊,CNCF TOC 成员,at 阿里巴巴。

[2] 《Kubernetes 权威指南》第五版

[3] 《Large-scale cluster management at Google with Borg》

research.google/pubs/pub434…

[4] www.redhat.com/zh/topics/c…?

[5] www.infoworld.com/article/363…?

[6] landscape.cncf.io/

[7] docs.docker.com/desktop/kub…

[8] minikube.sigs.k8s.io/docs/start/

[9] www.aliyun.com/product/kub…?

[10] github.com/kubernetes/…

[11] www.cnblogs.com/chiangchou/…

[12] github.com/tarosky/k8s…

[13] sdk.operatorframework.io/docs/instal…

原文链接

本文为阿里云原创内容,未经允许不得转载。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

859 亲密字符串 简单字符串模拟题

发表于 2021-11-23

「这是我参与11月更文挑战的第 23 天,活动详情查看:2021最后一次更文挑战」。

题目描述

这是 LeetCode 上的 859. 亲密字符串 ,难度为 简单。

Tag : 「模拟」

给你两个字符串 s 和 goal ,只要我们可以通过交换 s 中的两个字母得到与 goal 相等的结果,就返回 true ;否则返回 false 。

交换字母的定义是:取两个下标 i 和 j (下标从 000 开始)且满足 i != j ,接着交换 s[i] 和 s[j] 处的字符。

  • 例如,在 "abcd" 中交换下标 0 和下标 2 的元素可以生成 "cbad" 。

示例 1:

1
2
3
4
5
arduino复制代码输入:s = "ab", goal = "ba"

输出:true

解释:你可以交换 s[0] = 'a' 和 s[1] = 'b' 生成 "ba",此时 s 和 goal 相等。

示例 2:

1
2
3
4
5
arduino复制代码输入:s = "ab", goal = "ab"

输出:false

解释:你只能交换 s[0] = 'a' 和 s[1] = 'b' 生成 "ba",此时 s 和 goal 不相等。

示例 3:

1
2
3
4
5
arduino复制代码输入:s = "aa", goal = "aa"

输出:true

解释:你可以交换 s[0] = 'a' 和 s[1] = 'a' 生成 "aa",此时 s 和 goal 相等。

示例 4:

1
2
3
ini复制代码输入:s = "aaaaaaabc", goal = "aaaaaaacb"

输出:true

提示:

  • 1<=s.length,goal.length<=2∗1041 <= s.length, goal.length <= 2 * 10^41<=s.length,goal.length<=2∗104
  • s 和 goal 由小写英文字母组成

模拟

根据题意进行模拟即可,搞清楚什么情况下两者为「亲密字符」:

  1. 当 sss 与 goalgoalgoal 长度 或 词频不同,必然不为亲密字符;
  2. 当「sss 与 goalgoalgoal 不同的字符数量为 222(能够相互交换)」或「sss 与 goalgoalgoal 不同的字符数量为 000,但同时 sss 中有出现数量超过 222 的字符(能够相互交换)」时,两者必然为亲密字符。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Java复制代码class Solution {
public boolean buddyStrings(String s, String goal) {
int n = s.length(), m = goal.length();
if (n != m) return false;
int[] cnt1 = new int[26], cnt2 = new int[26];
int sum = 0;
for (int i = 0; i < n; i++) {
int a = s.charAt(i) - 'a', b = goal.charAt(i) - 'a';
cnt1[a]++; cnt2[b]++;
if (a != b) sum++;
}
boolean ok = false;
for (int i = 0; i < 26; i++) {
if (cnt1[i] != cnt2[i]) return false;
if (cnt1[i] > 1) ok = true;
}
return sum == 2 || (sum == 0 && ok);
}
}
  • 时间复杂度:令 nnn 为两字符串之间的最大长度,CCC 为字符集大小,CCC 固定为 262626,复杂度为 O(n+C)O(n + C)O(n+C)
  • 空间复杂度:O(C)O(C)O(C)

最后

这是我们「刷穿 LeetCode」系列文章的第 No.859 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:github.com/SharingSour… 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Linux里的“宝塔”,真正的宝塔!详细教程

发表于 2021-11-23

这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战

老表一起学云服务器开发相关文章:

先导篇:拥有有一台服务器后,我竟然这么酷?

替代项目:10行代码写一个简历页面!

和不安全访问 Say goodbye,手把手教大家如何给域名申请免费 SSL 证书

原创不易,求大家浏览到文章最后,点个赞、在看,留个言,随便说说都行,谢谢读者朋友支持,接下来开始正文。

一、为什么要装宝塔面板

宝塔Linux面板是提升运维效率的服务器管理软件,支持一键LAMP/LNMP/集群/监控/网站/FTP/数据库/JAVA等100多项服务器管理功能。

简单来理解,我们在Linux环境下只有靠命令行来进行各种操作,对于新手来说是非常不友好的,特别是要安装一些环境的时候,比如mysql、nginx等,宝塔面板是一个可视化(用啥点啥,不用输入指令)的界面,我们可以很方便的进行以上操作。

另外我们还可以利用宝塔进行nginx反向代理设置、更方便的上传/下载/修改/删除文件、快速设置定时任务等,真的超香,下面我们一起来体验吧~

二、安装宝塔面板,基础设置

首先,我们需要前往服务器安全组,添加8888端口的入权限,宝塔面板默认启动在8888端口,不知道怎么添加的看本系列第一篇文章:xxxxx。

然后我们复制下面的安装指令到服务器运行即可安装宝塔啦~(我这里是阿里云研发的Linux,如果你们安装的是其他系统可以看官网安装方法哈,如遇到问题,也可以留言说说你的系统和遇到的问题~)

1
bash复制代码yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && sh install.sh

我这边整个安装过程2分钟左右,安装成功后会提示访问地址以及初始化的用户名和密码。

浏览器打开页面,然后输入用户名和密码即可登录,用户协议下滑到最底部后同意即可。

进入后会先推荐大家安装开发套件(相关软件工具),这里我们只需要装nignx和mysql即可,想安装mysql8.0来着,但是提示内存需要6GB,所以我们就选myslq5.7吧。

不要觉得5.7是不是很老,和8.0隔了很多个版本哈,5.7的下一个版本就是8.0,关于为什么这样命名,我在v2ex上找到一个比较合理的解释。。。

点击一键安装,就会进入下载安装页面,估计要一会,这个时候你可以关闭安装界面。

过了软件安装你会发现需要注册宝塔账号,登录后才能使用,可以直接手机注册一个,也可以尝试后面去除宝塔面板登录限制方法(直接浏览器搜索即可,建议直接注册方便,和手机绑定,操作安全性也更高)。

如果选择注册一个宝塔账号的话,你可以通过我的链接进行注册登录,这样你可以领取宝塔3188元礼包,我这边也能有一定的收益(谢谢),注册链接:www.bt.cn/?invite_cod…

进入宝塔后,你可以看到机器的基本情况,以及宝塔界面的一些相关功能。

安装mysql和nginx一共花了13分钟。

如果后面你忘记了宝塔的默认登录账号和密码,可以在服务器里直接输入bt default查看(显示的只是最开始的初始密码和账号,其他情况忘记密码往后看)。

如果你修改账号/密码后,忘记了账号/密码,可以输入bt,进入面板功能选项,选择对应功能进行修改账号/密码。

三、宝塔面板进阶使用

3.1 创建配置网站

登录宝塔面板后,我们点击导航栏左侧的网站tab,即可进入添加网站页面,然后点击添加站点,即可输入要添加的网站域名、备注信息、相关配置文件存放根目录及其他信息。

完成基础设置后,还需要部署证书,点击查看如何给域名配置免费证书。点击未部署,就会自动跳转到部署页面。

在部署页面选择其他证书,将下载下来的证书的证书密钥key和证书内容pem复制放到对应的框内,然后点击保存按钮即可。

部署成功后,会显示证书相关信息和到期时间,到期后继续去阿里云/腾讯云申请即可,另外勾选上强制https。

都配置成功后,你直接访问配置的域名,如果跳转到下面nignx默认页面,就算配置成功啦~一个简单反向代理就完成啦,我们实现了客户端访问自己的域名实际返回的为服务器80端口的内容~

接下来,我们可以进一步对网站进行设置,我们点击设置按钮,即可进入网站设置页面,除了前面配置ssl证书外,还有域名管理、流量限制、重定向和反向代理等功能设置,下面简单和大家介绍下重定向和反向代理。

重定向就是访问网站域名,会跳转到我们设置的重定向域名,比如下面访问我们自己的域名,就会跳转到百度翻译。

反向代理指的是,访问我们的域名,域名不会变,但是显示内容会变成我们指定页面的内容,比如下面设置,访问域名,实际显示的内容会从原来的nginx默认页面,变成百度翻译的页面。

3.2 方便的文件操作

宝塔还可以让我们超便捷的处理服务器中的文件,以及本地和服务器文件的互相传输等。

在宝塔页面点击左侧的文件按钮,我们就可以进入文件管理页面了,我们可以自由选择进入哪个目录;从本地上传文件/文件夹到服务器;从远程其他服务器下载文件/文件夹到服务器;还可以设置分享文件夹/文件给其他人下载;直接在当前目录打开终端进行操作等。

3.2.1 从本地上传文件/文件夹到服务器

我们点击新建目录,用于存放演示文件。

1)进入到新建的文件夹(/root/Project/Temp),点击上传按钮,即可跳转到文件上传页面,

2)可以选择上传文件/文件夹,选择好上传类型后,在点击按钮即可选择本地文件进行上传,

3)注意它会先加载并显示待上传文件列表,我们确定没问题,再点击开始上传即可,上传完成后,并不会自动退出上传文件页面,如果还需上传其他文件按上述方法进行即可,如不继续上传,直接点击右上角❌即可退出上传文件页面。

另外还支持远程下载,点击远程下载按钮,输入远程文件地址,然后指定下载到服务器哪个目录及对应保存文件名;然后点击确认即可。

太方便啦,比自己在本地用scp命令方便太多啦~

3.2.2 从服务器下载文件/文件夹到本地

这个也很简单,对于单个文件,我们直接右键,即可选择直接下载到本地。

或者选择分享,系统会帮我们生成一个分享链接,我们还可以设置验证码密码,增加文件分享的安全性,设置后,我们将分享链接和密码给到下载的人即可。

文件夹无法直接下载,可以通过分享的方式进行下载文件内容,或者将文件夹压缩成压缩包文件后下载。

3.2.3 直接在当前目录打开终端

这个我也非常喜欢,直接在对应文件夹下打开终端,和我在mac上安装EasyNewFile后新建终端一样,打开终端的同时会自动进入对应的目录下。

有两种方式,在对应目录下直接点击终端按钮,或者在目录下空白处右键,选择终端即可。

3.2.4 我的最爱:修改文件相关操作

这个是我最爱的功能了,在宝塔文件管理里面,我们可以很方便的修改文件内容,一边喜欢linux的简单、炫酷,又嫌文件操作(特别是修改)麻烦的人就是我了。。。

双击对应的文件即可对文件内容进行修改,修改界面如下,简直太爱了,功能超级齐全,基础功能比如:保存、全部保存、刷新都有,进阶点的搜索、替换、跳转行、字体一样不落,更进阶的主题、设置、快捷键简直都是宝藏。

基础功能就不介绍了,大家直接体验即可,主题设置有白色和黑色两种,个人喜欢黑色,注意这个主题只改了文件修改区域的颜色,目录和工具栏没有变,个人看起来有点怪怪的~

设置里最宝藏的就是代码自动完成了,也就是大家熟悉的代码自动补全功能。

虽然不是那么好用,但是总比没有好,爱了爱了~

快捷键里大部分和我们本地文件操作的一样,所以用起来非常丝滑。

感觉简直就是sublime text的mini版,哦,对了,说了一堆功能还没说这么改文件内容,其实就是和本地修改一样,直接文件里想改哪里,光标移到哪里就行~

3.3 如此简单的设置定时任务

接下来想给大家介绍的就是,计划任务这个功能了,在这里你可以很便捷的设置定时任务,也就是每到一个时间让系统自动执行某个程序或者脚本。

宝塔里面提供了非常多的任务类型,比如Shell脚本、备份网站、备份数据库、日志切割、释放内存、访问URL…对于一般的云服务器使用者,我想Shell脚本这个类型是最长用的,其他的咱暂时也不懂。

所谓Shell脚本就是一个linux命令(其他任务也是linux命令,比较复杂的命令),比如我们要定时运行python代码。

以我最近使用的定时任务为例子,在赠送阿里云服务器活动中,有一个步骤是验证大家是否满足获赠资格,怎么验证的呢?

在大家点击我的链接关联我后,阿里云后台会有对应的关联信息(但是他们没做用户验证页面),我需要在阿里云爬取到关联我的用户基本信息,然后大家再通过我写的web页面去查询看看信息是否匹配。

在这个过程有一项就是:更新数据,只有我这边数据更新后,关联了的用户才能在页面查询的时候查询到是否满足资格。

如果要及时更新,我一天到晚给大家更新数据,那我这一天就没了~

每半天或者一天给大家更新一次数据,好像可以,但是如果大家误操作,购买了什么0元商品,就不是新用户了,可能失去赠送资格~

写个python脚本定时更新数据,可以,不过麻烦~(其实也可哈哈哈哈)

这个时候,我们直接使用宝塔的计划任务功能,设置一个定时的Shell脚本即可,设置好名称(随便写,达意)、执行周期、脚本内容(确保自己终端执行没问题),就可以点击添加任务按钮,添加一个定时任务了,全程不到10s~(python写定时任务脚本没这么快吧~)

设置完成后,我们需要先点击执行测试执行一遍,然后查看日志,确保脚本运行没问题。

软件商店里面可以下载一些应用或者服务环境,比如mysql、php等,面板设置里面可以设置修改面板的一些基本内容,如:登录端口、密码、用户名等;其他功能我就不一一介绍啦,大家直接安装后去体验即可,绝对超乎想象~

四、下期预告

通过本文学习,我们找到了一个学习使用云服务器的好帮手–宝塔,希望大家能掌握宝塔的一些优秀功能使用方法,真正让工具帮助到我们学习,当然,站在学习者的角度,我们也不能过分依赖工具,要“知其然知其所以然”。

在下一节中,我们将进一步探索学习云服务器的使用,你想听关于云服务器的哪些方面内容呢?欢迎大家在留言区说出自己的想法,我将选择点赞或者提出多的方向提前输出。

老表一起学云服务器开发相关文章:

先导篇:拥有有一台服务器后,我竟然这么酷?

替代项目:10行代码写一个简历页面!

和不安全访问 Say goodbye,手把手教大家如何给域名申请免费 SSL 证书

原创不易,点个赞、在看,留个言,随便说说都行,谢谢读者朋友支持。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

gobuffalo介绍

发表于 2021-11-23

「这是我参与11月更文挑战的第23天,活动详情查看:2021最后一次更文挑战」。

虽然Buffalo可以被认为是一个框架,但它主要是Go和Javascript库精心组合而成的生态系统。这些组件中的大多数可以切换为另一个组件,但是我们将仅对此默认组合提供支持

1 安装数据库连接环境

特别注意:从2019年12月1日开始,Buffalo和所有相关软件包都需要Go Modules,并且GOPATH不再支持使用该模块。$GOPATH$并且最低Go版本为1.13具体说明链接

1.1 安装环境必需的依赖项

  • 可行的Go环境
  • 已配置的PATH环境变量,包括PATH环境变量,包括PATH环境变量,包括GOPATH/bin。
  • 转到版本>=1.13。

2 pop基础安装及使用

pop是gobuffalo默认的orm 包,当然也可以在其他项目使用pop。但是pop官方强调自己不是orm,只是具备orm的功能。

::: tip pop主要优点

  • CRUD 操作
  • 代码定义模型
  • 用于创建,删除和修改的迁移工具
  • 数据库支持:PostgreSQL,MySQL,SQLite
  • ActiveRecord UUID 模式
  • YAML 配置
  • 易于环境变量使用
  • 创建和更新每条记录的时间戳
  • 支持事务
    :::

2.1 soda安装

大多数Golang软件包的安装都是典型的,但是我们还将安装soda实用程序,该实用程序将促进数据库迁移和模型创建。

不需要使用sqlite 3数据库支持,例如使用mysql,执行以下命令安装soda

1
2
bash复制代码$ go get github.com/gobuffalo/pop/...
$ go install github.com/gobuffalo/pop/soda

需要sqlite 3支持(需要GCC或等效的C编译器),执行以下命令安装:

1
2
bash复制代码$ go get -u -v -tags sqlite github.com/gobuffalo/pop/...
$ go install -tags sqlite github.com/gobuffalo/pop/soda

如果您不使用构建代码buffalo build,则在构建程序时还必须传递-tags sqlite给go build。

2.2 pop配置

通过YAML文件配置弹出。每个节均按环境细分,因此您可以针对每个环境进行配置。在您的项目根目录中,创建文件database.yml或者config/database.yml,或者自定义配置文件。可以通过将-e development标志传递到任何命令中来标识开发环境。

1
2
3
4
5
6
7
yaml复制代码development:
dialect: "mysql" #数据库类型
database: "kratos" #数据库名称
host: "localhost" #数据库连接地址
port: "3306" #数据库连接端口号
user: "root" #数据库连接用户名账号
password: "admin" #数据库连接账号密码

2.3 创建模型

pop提供一个实用工具soda。用soda可以创建模型。

1
2
3
4
5
6
7
8
bash复制代码$ soda generate model user title:string first_name:string last_name:string bio:text -e development
v3.41.1

--> models/user.go
--> models/user_test.go
--> goimports -w models/user.go models/user_test.go
> migrations/20191225173819_create_users.up.fizz
> migrations/20191225173819_create_users.down.fizz

soda 创建了两个文件夹:models 和 migrations。用户模型存储在其中 models/user.go,初始迁移存储在 migrations/20191225173819_create_users.up.fizz。fizz 下面会讲到如何使用。

2.4 创建数据库

1、在执行soda命令根目录下有database.yml或者config/database.yml时,并且数据库服务器正在运行,Soda可以在database.yml使用一个简单的命令在文件中创建所有数据库,如下:
-e development 代表创建开发环境下的数据库,当然也可以是 test 和 production。

1
bash复制代码$ soda create -e development

2、在执行soda命令根目录下 没有database.yml或者config/database.yml 时,则必须指定配置文件路径创建数据库,如下

1
bash复制代码soda create -e development -c ./contrib/sql/.soda.yml

2.5 完成 fizz

编辑 migrations/20191225173819_create_users.up.fizz 文件,下面代码相当于创建一张user表 然后添加两条数据。
具体数据操作语法请参考gobuffalo官网

1
2
3
4
5
6
7
8
9
10
11
12
bash复制代码create_table("users") {
t.Column("id", "uuid", {primary: true})
t.Timestamps()
t.Column("name", "string", {})
t.Column("email", "string", {})
t.Column("password_hash", "string", {"size":64})
t.Column("register_time", "timestamp", {"default": null})
t.Column("last_login_time", "timestamp", {"default": null})
}

add_index("users", "name", {"unique": true})
add_index("users", "email", {"unique": true})

2.6 运行迁移

1 在执行soda migrate命令迁移数据时,根目录下有database.yml或者config/database.yml时,并且数据库服务器正在运行,soda可以使用下面命令迁移数据库,迁移的数据表位置在执行soda命令位置的./migrations文件夹下面的所有fizz后缀的文件

1
bash复制代码soda migrate up -e development

2 在执行soda migrate命令迁移数据时,根目录下 没有database.yml或者config/database.yml 时,则必须指定迁移配置的yml,迁移的文件路径在自定义的yml目录的./migrations文件夹下面的所有fizz后缀的文件

1
bash复制代码soda migrate up -e development -c ./contrib/sql/.soda.yml

这样,我们的数据库,数据表都已经创建完成了。

3 Golang操作数据库说明

3.1 连接数据库

1
2
3
4
go复制代码tx, err := pop.Connect("development")
if err != nil {
log.Panic(err)
}

3.2 创建新纪录

1
2
3
4
5
6
7
8
9
10
go复制代码jessica := models.User{
Title: "Ms.",
FirstName: "Jessica",
LastName: "Jones",
Bio: "Private security, super hero.",
}
_, err = tx.ValidateAndSave(&jessica)
if err != nil {
log.Panic(err)
}

3.3 按 ID 查询一条记录

1
2
3
4
5
6
7
8
9
10
go复制代码  id := "240ec3c5-019d-4031-9c27-8a553e022297"
frank := models.User{}
err = tx.Find(&frank, id)
if err != nil {
fmt.Print("ERROR!\n")
fmt.Printf("%v\n", err)
} else {
fmt.Print("Success!\n")
fmt.Printf("%v\n", frank)
}

3.4 查询所有记录

1
2
3
4
5
6
7
8
9
go复制代码users := []models.User{}
err = tx.All(&users)
if err != nil {
fmt.Print("ERROR!\n")
fmt.Printf("%v\n", err)
} else {
fmt.Print("Success!\n")
fmt.Printf("%v\n", users)
}

3.5 模糊查询某些数据

1
2
3
4
5
6
7
8
9
10
go复制代码query := tx.Where("last_name = 'Rand' OR last_name = 'Murdock'")
users := []models.User{}
err = query.All(&users)
if err != nil {
fmt.Print("ERROR!\n")
fmt.Printf("%v\n", err)
} else {
fmt.Print("Success!\n")
fmt.Printf("%v\n", users)
}

3.6 更新单个记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go复制代码 query := tx.Where("title = 'Ms.'")
users := []models.User{}
err = query.All(&users)
if err != nil {
fmt.Print("ERROR!\n")
fmt.Printf("%v\n", err)
} else {
for i := 0; i < len(users); i++ {
user := users[i]
user.Title = "Mrs."
tx.ValidateAndSave(&user)
fmt.Print("Success!\n")
fmt.Printf("%v\n", user)
}
}

3.7 更新多个记录

更新多个记录与更新单个记录非常相似

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码users := []models.User{}
err = tx.All(&users)
if err != nil {
fmt.Print("ERROR!\n")
fmt.Printf("%v\n", err)
} else {
for i := 0; i < len(users); i++ {
user := users[i]
user.Location = "NYC, NY"
tx.ValidateAndSave(&user)
fmt.Print("Success!\n")
fmt.Printf("%v\n", user)
}
}

3.8 删除单个记录

1
2
3
4
5
6
7
8
9
10
go复制代码  id := "240ec3c5-019d-4031-9c27-8a553e022297"
frank := models.User{}
err = tx.Find(&frank, id)
if err != nil {
fmt.Print("ERROR!\n")
fmt.Printf("%v\n", err)
} else {
fmt.Print("Success! - Now delete it.\n")
tx.Destroy(&frank)
}

4 迁移数据到mysql示例

备注:从这里开始使用Goland编译器来操作kratos项目

4.1 用Navicat创建一个数据库kratos

创建数据库kratos

4.2 进行kratos项目数据迁移

4.2.1 项目路径在%GOPATH%\src\kratos\persistence\sql\

kratos项目数据迁移准备

4.2.2 输入以下命令,对数据库运行所有“向上”迁移

1
bash复制代码soda migrate up -e development -c ./persistence/sql/.soda.yml

对数据库运行所有“向上”迁移

4.3 在Navicat查看是否迁移成功

查看是否迁移成功

4.4 修改kratos的配置文件数据连接

路径在$GOPATH$\src\kratos\docs\.kratos.yaml
 修改kratos的配置文件

5 更多资料参考

gobuffalo官网

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

nginx 反向代理 【2】

发表于 2021-11-23

「这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战」。

往期文章:

初识 Nginx

nginx 的安装

nginx 核心配置文件结构

nginx 静态资源部署 【1】

nginx 静态资源部署 【2】

nginx 静态资源部署 【3】

nginx 静态资源部署 【4】

nginx 反向代理 【1】

Nginx的安全控制

关于web服务器的安全是比较大的一个话题,里面所涉及的内容很多,Nginx反向代理是如何来提升web服务器的安全呢?

1
复制代码安全隔离

什么是安全隔离?

通过代理分开了客户端到应用程序服务器端的连接,实现了安全措施。在反向代理之前设置防火墙,仅留一个入口供代理服务器访问。

111.jpg

如何使用SSL对流量进行加密

翻译成大家能熟悉的说法就是将我们常用的http请求转变成https请求,那么这两个之间的区别简单的来说两个都是HTTP协议,只不过https是身披SSL外壳的http.

HTTPS是一种通过计算机网络进行安全通信的传输协议。它经由HTTP进行通信,利用SSL/TLS建立全通信,加密数据包,确保数据的安全性。

SSL(Secure Sockets Layer)安全套接层

TLS(Transport Layer Security)传输层安全

上述这两个是为网络通信提供安全及数据完整性的一种安全协议,TLS和SSL在传输层和应用层对网络连接进行加密。

总结来说为什么要使用https:

1
复制代码http协议是明文传输数据,存在安全问题,而https是加密传输,相当于http+ssl,并且可以防止流量劫持。

Nginx要想使用SSL,需要满足一个条件即需要添加一个模块--with-http_ssl_module,而该模块在编译的过程中又需要OpenSSL的支持,这个我们之前已经准备好了。

nginx添加SSL的支持

(1)完成 --with-http_ssl_module模块的增量添加

1
2
3
4
5
6
bash复制代码》将原有/usr/local/nginx/sbin/nginx进行备份
》拷贝nginx之前的配置信息
》在nginx的安装源码进行配置指定对应模块 ./configure --with-http_ssl_module
》通过make模板进行编译
》将objs下面的nginx移动到/usr/local/nginx/sbin下
》在源码目录下执行 make upgrade进行升级,这个可以实现不停机添加新模块的功能
Nginx的SSL相关指令

因为刚才我们介绍过该模块的指令都是通过ngx_http_ssl_module模块来解析的。

ssl:该指令用来在指定的服务器开启HTTPS,可以使用 listen 443 ssl,后面这种方式更通用些。

| 语法 | ssl on | off; |
| — | — |
| 默认值 | ssl off; |
| 位置 | http、server |

1
2
3
arduino复制代码server{
listen 443 ssl;
}

ssl_certificate:为当前这个虚拟主机指定一个带有PEM格式证书的证书。

语法 ssl_certificate file;
默认值 —
位置 http、server

ssl_certificate_key:该指令用来指定PEM secret key文件的路径

语法 ssl_ceritificate_key file;
默认值 —
位置 http、server

ssl_session_cache:该指令用来配置用于SSL会话的缓存

| 语法 | ssl_sesion_cache off|none|[builtin[:size]] [shared:name:size] |
| — | — |
| 默认值 | ssl_session_cache none; |
| 位置 | http、server |

off:禁用会话缓存,客户端不得重复使用会话

none:禁止使用会话缓存,客户端可以重复使用,但是并没有在缓存中存储会话参数

builtin:内置OpenSSL缓存,仅在一个工作进程中使用。

shared:所有工作进程之间共享缓存,缓存的相关信息用name和size来指定

ssl_session_timeout:开启SSL会话功能后,设置客户端能够反复使用储存在缓存中的会话参数时间。

语法 ssl_session_timeout time;
默认值 ssl_session_timeout 5m;
位置 http、server

ssl_ciphers:指出允许的密码,密码指定为OpenSSL支持的格式

语法 ssl_ciphers ciphers;
默认值 ssl_ciphers HIGH:!aNULL:!MD5;
位置 http、server

可以使用openssl ciphers查看openssl支持的格式。

ssl_prefer_server_ciphers:该指令指定是否服务器密码优先客户端密码

| 语法 | ssl_perfer_server_ciphers on|off; |
| — | — |
| 默认值 | ssl_perfer_server_ciphers off; |
| 位置 | http、server |

生成证书

方式一:使用阿里云/腾讯云等第三方服务进行购买。

方式二:使用openssl生成证书

先要确认当前系统是否有安装openssl

1
复制代码openssl version

安装下面的命令进行生成

1
2
3
4
5
6
7
vbnet复制代码mkdir /root/cert
cd /root/cert
openssl genrsa -des3 -out server.key 1024
openssl req -new -key server.key -out server.csr
cp server.key server.key.org
openssl rsa -in server.key.org -out server.key
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
开启SSL实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ini复制代码server {
  listen       443 ssl;
  server_name localhost;

  ssl_certificate     server.cert;
  ssl_certificate_key server.key;

  ssl_session_cache   shared:SSL:1m;
  ssl_session_timeout 5m;

  ssl_ciphers HIGH:!aNULL:!MD5;
  ssl_prefer_server_ciphers on;

  location / {
      root   html;
      index index.html index.htm;
  }
}

(4)验证

反向代理系统调优

反向代理值Buffer和Cache

Buffer翻译过来是”缓冲”,Cache翻译过来是”缓存”。

111.jpg

总结下:

1
2
3
4
5
makefile复制代码相同点:
两种方式都是用来提供IO吞吐效率,都是用来提升Nginx代理的性能。
不同点:
缓冲主要用来解决不同设备之间数据传递速度不一致导致的性能低的问题,缓冲中的数据一旦此次操作完成后,就可以删除。
缓存主要是备份,将被代理服务器的数据缓存一份到代理服务器,这样的话,客户端再次获取相同数据的时候,就只需要从代理服务器上获取,效率较高,缓存中的数据可以重复使用,只有满足特定条件才会删除.

(1)Proxy Buffer相关指令

proxy_buffering :该指令用来开启或者关闭代理服务器的缓冲区;

| 语法 | proxy_buffering on|off; |
| — | — |
| 默认值 | proxy_buffering on; |
| 位置 | http、server、location |

proxy_buffers:该指令用来指定单个连接从代理服务器读取响应的缓存区的个数和大小。

语法 proxy_buffers number size;
默认值 proxy_buffers 8 4k
位置 http、server、location

number:缓冲区的个数

size:每个缓冲区的大小,缓冲区的总大小就是number*size

proxy_buffer_size:该指令用来设置从被代理服务器获取的第一部分响应数据的大小。保持与proxy_buffers中的size一致即可,当然也可以更小。

语法 proxy_buffer_size size;
默认值 proxy_buffer_size 4k
位置 http、server、location

proxy_busy_buffers_size:该指令用来限制同时处于BUSY状态的缓冲总大小。

语法 proxy_busy_buffers_size size;
默认值 proxy_busy_buffers_size 8k
位置 http、server、location

proxy_temp_path:当缓冲区存满后,仍未被Nginx服务器完全接受,响应数据就会被临时存放在磁盘文件上,该指令设置文件路径

语法 proxy_temp_path path;
默认值 proxy_temp_path proxy_temp;
位置 http、server、location

注意path最多设置三层。

proxy_temp_file_write_size:该指令用来设置磁盘上缓冲文件的大小。

语法 proxy_temp_file_write_size size;
默认值 proxy_temp_file_write_size 8K
位置 http、server、location

通用网站的配置

1
2
3
4
ini复制代码proxy_buffering on;
proxy_buffer_size 4 32k;
proxy_busy_buffers_size 64k;
proxy_temp_file_write_size 64k;

根据项目的具体内容进行相应的调节。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…221222223…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%