首页 文章 精选 留言 我的

精选列表

搜索[学习],共10003篇文章
优秀的个人博客,低调大师

Elasticsearch学习-父子文档

本文以Elasticsearch 6.8.4版本为例,介绍Elasticsearch父子文档的使用。 上一篇文章介绍了Elasticsearch的嵌套文档,这一篇来介绍另外一种关系文档,父子文档。 1、父子文档 父子文档在理解上来说,可以理解为一个关联查询,有些类似MySQL中的JOIN查询,通过某个字段关系来关联。 父子文档与嵌套文档主要的区别在于,父子文档的父对象和子对象都是独立的文档,而嵌套文档中都在同一个文档中存储,如下图所示: 这里引用官网的话,对比嵌套文档来说,父-子关系的主要优势有: 更新父文档时,不会重新索引子文档。 创建,修改或删除子文档时,不会影响父文档或其他子文档。这一点在这种场景下尤其有用:子文档数量较多,并且子文档创建和修改的频率高时。 子文档可以作为搜索结果独立返回。 1.1 创建索引 这里还是以嵌套文档的数据为例,假设数据如下: [ { "title":"这是一篇文章", "body":"这是一篇文章,从哪里说起呢? ... ..." }, { "name":"张三", "comment":"写的不错", "age":28, "date":"2020-05-04" }, { "name":"李四", "comment":"写的很好", "age":20, "date":"2020-05-04" }, { "name":"王五", "comment":"这是一篇非常棒的文章", "age":31, "date":"2020-05-01" } ] 创建索引名和type均为blog的索引,从上面数据可以看出,其实父文档(博客内容)与子文档分别用不同的字段来存储对应的数据,不过在创建索引文档的时候需要指定父子文档的关系,即文章为parent,留言为child,创建索引语句如下: PUT http://localhost:9200/blog/ { "mappings": { "blog": { "properties": { "date": { "type": "date" }, "name": { "type": "text", "fields": { "keyword": { "type": "keyword" } } }, "comment": { "type": "text", "fields": { "keyword": { "type": "keyword" } } }, "age": { "type": "long" }, "body": { "type": "text", "fields": { "keyword": { "type": "keyword" } } }, "title": { "type": "text", "fields": { "keyword": { "type": "keyword" } } }, "relation": { "type": "join", "relations": { "parent": "child" } } } } } } 如下图所示 1.2 插入数据 插入父文档数据,需要指定上文索引结构中的relation为parent,如下: POST http://localhost:9200/blog/blog/1/ { "title":"这是一篇文章", "body":"这是一篇文章,从哪里说起呢? ... ...", "relation":"parent" } 插入子文档,需要在请求地址上使用routing参数指定是谁的子文档,并且指定索引结构中的relation关系,如下: POST http://localhost:9200/blog/blog/2?routing=1 { "name":"张三", "comment":"写的不错", "age":28, "date":"2020-05-04", "relation":{ "name":"child", "parent":1 } } POST http://localhost:9200/blog/blog/3?routing=1 { "name":"李四", "comment":"写的很好", "age":20, "date":"2020-05-04", "relation":{ "name":"child", "parent":1 } } POST http://localhost:9200/blog/blog/4?routing=1 { "name":"王五", "comment":"这是一篇非常棒的文章", "age":31, "date":"2020-05-01", "relation":{ "name":"child", "parent":1 } } 插入完成后,如下图所示。 从这里其实可以很明显的看出与嵌套文档的区别了,嵌套文档只有一个文档,而这里是有四个文档。 1.3 查询 普通查询这里不进行赘述,关系查询的话其实很好理解,大致分为两种特殊情况: 根据父文档查询子文档 has_child 根据子文档查询父文档 has_parent 接下来我们来看如何进行关系查询,首先看一下通过子文档查询父文档,比如这样的场景,查询名称是张三的人留言的文章,查询语句如下: { "query": { "has_child": { "type":"child", "query": { "match": { "name": "张三" } } } } } 查询结果如下: 使用has_child来根据子文档内容查询父文档,其实type就是创建文档时,子文档的标识。 在使用子查父的时候,可以添加一些筛选条件来增强匹配的结果,比如最大匹配max_children和最小匹配min_children,这里有点类似should查询的minimum_should_match,感兴趣的可以去官网了解更多的细节。 到这里,其实对Elasticsearch特性了解的读者就会知道如何根据父文档查询子文档了,只需要注意一点,父查子type需要修改成parent_type,其余都与自查父类似,比如查询标题为“这是一篇文章”的数据的留言内容,查询语句如下: { "query": { "has_parent": { "parent_type":"parent", "query": { "match": { "title": "这是一篇文章" } } } } } 查询结果如下: 由于只有一组父子文档,效果不是很明显,感兴趣可以多造一些数据去体验 聚合查询与嵌套文档类似,比较简单,这里在说明另外一种场景:祖辈和孙辈可以创建吗?比如本文中的留言如果它也有子文档,那么可以根据文章查询孙辈吗?答案是可以的,只需要在has_child里面在嵌套一层has_child查询即可。 1.4 使用建议 父子文档都可以独立返回,对于某些场景很适用,比如主表信息是一些基本不变的数据,而子表信息经常增删改,并且子表信息经常有查询场景,这样就很适合使用父子文档。 父子文档需要在同一分片上,当然,我们无需做特殊处理,默认就会为我放入同一个分片,其实原理是这样的,Elasticsearch会根据routing中的参数去看父文档所在分片在哪,然后将对应文档存储进去。 父子文档查询效率相对嵌套文档较低,官网说是5-10倍左右。 其余官网也给定了一些建议,具体可以查看官方文档,地址:https://www.elastic.co/guide/cn/elasticsearch/guide/current/parent-child-performance.html

优秀的个人博客,低调大师

如何轻松学习 Kubernetes?

作者 | 声东 阿里巴巴技术专家 <关注阿里巴巴云原生公众号,回复排查即可下载电子书> 导读:《深入浅出 Kubernetes》一书共汇集 12 篇技术文章,帮助你一次搞懂 6 个核心原理,吃透基础理论,一次学会 6 个典型问题的华丽操作! 什么是 Kubernetes? 我们来看一下什么是 Kubernetes。这部分内容我会从四个角度来跟大家分享一下我的看法。 1. 未来什么样 这是一张未来大部分公司后端 IT 基础设施的架构图。简单来说,以后所有公司的 IT 基础设施都会部署在云上。用户会基于 Kubernetes 把底层云资源分割成具体的集群单元,给不同的业务使用。而随着业务微服务化的深入,服务网格这样的服务治理逻辑会变得跟下边两层一样,成为基础设施的范畴。 目前,阿里基本上所有的业务都跑在云上。而其中大约有一半的业务

优秀的个人博客,低调大师

java源码学习---HashMap

开门见山,直接干 HashMap是java常用的一个集合,每个元素的key经过哈希算法后储存在链表或红黑树的一种键值对数据集合(JDK1.8) 从HashMap新增元素说起 map.put("key","value"); 这是我们日常向HashMap插入元素的其中一种方式,put(k,v)的源码 public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } put()会再调用一个putVal(),但是在这之前key会通过hash()计算出对应位置的值,真正的put操作,就是从这里开始 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; } 每个参数的含义 hash:key经过哈希算法之后获得的值 key:储存到HashMap的key value:储存到HashMap的key对应的value onlyIfAbsent:如果包含了该key,则不更新对应的值,众所周知,put()除了新增之外,还有更新的功能,是因为put()调用putVal()时候传的都是false evict:HashMap是否处于创建模式,false则是处于创建模式,ture则相反 在函数最开始定义了几个变量, Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); tab:当前HashMap的一个操作副本, p:当前需要储存的元素或是当前相同数组下标的元素 n:当前HashMap的长度 i:当前元素储存的下标 Node<k,v>是HashMap的一个静态内部类,每一个键值对数据都是基于Node或TreeNode储存在HashMap Node: Node里面记录着当前元素的hash值、key、value还有下一个结点的地址 TreeNode: Node是TreeNode的父类,TreeNode还记录着父节点、左右子节点、 继续看下面的代码, 1.判断当前HashMap的容量大小,如果table是null或者容量为0会利用resize()进行扩容处理 2.判断tab[i](当前元素所储存的位置)是否为null,如果是null的情况下则直接保存到对应位置,并且把tab[i]赋值给p if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); 如果tab[i]不为null我们继续往下看,有以下几种情况 1.当table(链表)已存在结点与当前需要保存结点的key相同时,则更新值 2.判断当前结点是否为TreeNode,如果属于TreeNode则进入红黑树的储存规则判断,如果树里已经存在相同的key则返回旧的结点,否则直接在树上新增一个结点并返回null 3.当table(链表)里不存在相同的key且hash值一致的位置不属于TreeNode而属于Node时,遍历每个结点,判断与当前保存结点的key是否一致,一致的时候更新,否则添加到下一个结点 3.1添加完成后,判断当前元素所在index是否 >=TREEIFY_THRESHOLD - 1,如果大于,则需要从链表转为红黑树 static final int TREEIFY_THRESHOLD = 8; else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } 上面第1和第2点赋值的"e"如果不为null,则在此处进行更新操作并且返回旧值 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } 最后就是容量记录和扩容操作,如果是新增元素,HashMap会记录当前的容量,判断当前容量是否达到了需要扩容的阈值从而觉得是否进行扩容操作 ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; 总结: 1.利用hash()对储存的key做散列算法获得哈希值 2.新增元素时候,会判断当前HashMap的容量是否为0或者null,如果是则先进行扩容操作 3.当前hash值所在的链表或者树是否具有相同的key,没有则直接根据链表或红黑树的规则新增,否则修改结点的value并且把旧值返回 4.当其中一个链表长度大于等于8时候,会利用treeifyBin()转换成红黑树 5.新增成功之后会判断当前HashMap的容量是否达到了需要扩容的阈值,并决定是否需要扩容,决定是否扩容的阈值不是HashMap的容量最大值 删除 remove() public V remove(Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; } 根据对key做hash计算,并且调用removeNode()执行删除,如果能命中则返回删除的key对应的value final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node<K,V>[] tab; Node<K,V> p; int n, index; if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { Node<K,V> node = null, e; K k; V v; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; else if ((e = p.next) != null) { if (p instanceof TreeNode) node = ((TreeNode<K,V>)p).getTreeNode(hash, key); else { do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } } if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { if (node instanceof TreeNode) ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); else if (node == p) tab[index] = node.next; else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } } return null; } 参数含义: hash:利用hash()对key进行计算得出的结果 key:需要删除的key value:需要删除key对呀的value matchValue:是否需要匹配值相等,如果为ture则需要value相等才删除,否则不需要value相等 movable:是否移动树结点,为true时删除树时候移动结点,否则不移动结点 开始逐句解读: 1.判断当前HashMap是否为null或者元素数量为0, 2.判断当前储存在与key的hash值相同位置的结点是否为null,并且赋值hash相同的首个元素给 p 当上面其中一点不满足的情况下,则代表HashMap无当前需要删除的key,则直接返回null if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { 如果满足则继续往下执行,判断当前p的key是否与删除的key相同,相同则把值赋给变量node,最后对node进行删除操作 Node<K,V> node = null, e; K k; V v; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; 上面key如果不一样则会遍历整个链表或者红黑树,找到相同元素赋值给变量node,后续进行删除操作 else if ((e = p.next) != null) { if (p instanceof TreeNode) node = ((TreeNode<K,V>)p).getTreeNode(hash, key); else { do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } } 最后是判断有没有找到需要删除的node,并且判断key对呀的value是否需要相等与是否相等,相等则进行删除操作 如果是树结点,则按照红黑树的删除规则进行删除 如果是链表,则按照链表的删除规则进行删除 删除成功后记录当前元素数量,并且把删除的结点返回 不过源码中没找到有删除元素后重置容量的代码,这点与预想的有点不一样 if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { if (node instanceof TreeNode) ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); else if (node == p) tab[index] = node.next; else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } 扩容 resize() 在新增元素或者我new HashMap()的时候,我们都会调用到resize() 当容器容量达到一个需要扩容的阈值时,就会调用resize()进行扩容操作 先看一下HashMap定义的属性 1.扩容阈值,当容量大于该阈值时,HashMap会进行扩容操作 int threshold; 2.HashMap的最大容量:1073741824 static final int MAXIMUM_CAPACITY = 1 << 30; 3.HashMap默认初始容量:16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 4.负载因子,主要用于计算扩容阈值,扩容阈值 = 负载因子 * 当前容量 static final float DEFAULT_LOAD_FACTOR = 0.75f; 接下来是resize()的源码 final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; } 首先定义了各种变量,当前HashMap的副本、当前副本容量(旧容量)、当前副本扩容阈值(旧扩容阈值)、新的扩容阈值、新的容量 Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; 判断当前容量是否大于0 1.如果大于0并且容量大于容器最大容量,则不再扩容 2.如果少于最大容量且大于初始容量,则扩大2倍且扩容因子也扩大2倍 if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } 当前容量是否<=0时再判断当前扩容阈值是否大于0,大于0则新容量=旧阈值 当前容量和阈值=0时,基本可以判断这个HashMap是第一次初始化容量,所以直接用默认的初始化容量,和第一次计算扩容阈值(负载因子*当前容量) else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } 获取到最新容量和最新的扩容阈值时候,就开始对旧table进行扩容(新建一个数组,再把旧table的结点重新储存到新的数组) threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; 这一段则是把旧table上的结点重新储存到新table上 if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } 总结: 1.如果是新创建的HashMap初始化容量为16,每扩容一次则是原容量的2倍 2.触发扩容并不是当容量达到最大值才进行扩容,而是达到某个阈值而进行扩容 3.当容量达到HashMap的最大容量值时,将不再继续扩容 4.每次扩容结点都会重新储存到新的容器,资源消耗比较大

优秀的个人博客,低调大师

Kubernetes架构学习笔记

Kubernetes是Google开源的容器集群管理系统,其提供应用部署、维护、 扩展机制等功能,利用Kubernetes能方便地管理跨机器运行容器化的应用,是Docker分布式系统的解决方案。k8s里所有的资源都可以用yaml或Json定义。 1 K8s基本概念 1.1 Master Master节点负责整个集群的控制和管理,所有的控制命令都是发给它,上面运行着一组关键进程: kube-apiserver:提供了HTTP REST接口,是k8s所有资源增删改查等操作的唯一入口,也是集群控制的入口。 kube-controller-manager:所有资源的自动化控制中心。当集群状态与期望不同时,kcm会努力让集群恢复期望状态,比如:当一个pod死掉,kcm会努力新建一个pod来恢复对应replicas set期望的状态。 kube-scheduler:负责Pod的调度。 实际上,Master只是一个名义上的概念,三个关键的服务不一定需要运行在一个节点上。 1.1.1 API Server的原理 集群中的各个功能模块通过 apiserver将信息存储在Etcd,当需要修改这些信息的时候通过其REST接口来实现。 1.1.2 Controller Manager的原理 内部包含: Replication Controller Node Controller ResourceQuota Controller Namespace Controller ServiceAccount Controller Token Controller Service Controller Endpoint Controller等 这些Controller通过API Server实时监控各个资源的状态,当有资源因为故障导致状态变化,Controller就会尝试将系统由“现有状态”恢复到“期待状态”。 1.1.3 Scheduler的原理 作用是将apiserver或controller manager创建的Pod调度和绑定到具体的Node上,一旦绑定,就由Node上的kubelet接手Pod的接下来的生命周期管理。 1.2 Node Node是工作负载节点,运行着Master分配的负载(Pod),但一个Node宕机时,其上的负载会被自动转移到其他Node上。其上运行的关键组件是: kubelet:负责Pod的生命周期管理,同时与Master密切协作,实现集群管理的基本功能。 kube-proxy:实现Service的通信与负载均衡机制的重要组件,老版本主要通过设置iptables规则实现,新版1.9基于kube-proxy-lvs 实现。 Docker Engine:Docker引擎,负责Docker的生命周期管理。 1.2.1 kube-proxy的原理 每个Node上都运行着一个kube-proxy进程,它在本地建立一个SocketServer接收和转发请求,可以看作是Service的透明代理和负载均衡器,负载均衡策略模式是Round Robin。也可以设置会话保持,策略使用的是“ClientIP”,将同一个ClientIP的请求转发同一个Endpoint上。 Service的Cluster IP和NodePort等概念都是kube-proxy服务通过Iptables的NAT转换实现,Iptables机制针对的是kube-proxy监听的端口,所以每个Node上都要有kube-proxy。 1.2.2 kubelet原理 每个Node都会启动一个kubelet,主要作用有: (1)Node管理 注册节点信息; 通过cAdvisor监控容器和节点的资源; 定期向Master(实际上是apiserver)汇报本节点资源消耗情况 (2)Pod管理 所以非通过apiserver方式创建的Pod叫Static Pod,这里我们讨论的都是通过apiserver创建的普通Pod。kubelet通过apiserver监听etcd,所有针对Pod的操作都会被监听到,如果其中有涉及到本节点的Pod,则按照要求进行创建、修改、删除等操作。 (3)容器健康检查 kubelet通过两类探针检查容器的状态: LivenessProbe:判断一个容器是否健康,如果不健康则会删除这个容器,并按照restartPolicy看是否重启这个容器。实现的方式有ExecAction(在容器内部执行一个命令)、TCPSocketAction(如果端口可以被访问,则健康)、HttpGetAction(如果返回200则健康)。 ReadinessProbe:用于判断容器是否启动完全。如果返回的是失败,则Endpoint Controller会将这个Pod的Endpoint从Service的Endpoint列表中删除。也就是,不会有请求转发给它。 1.3 Pod Pod是k8s进行资源调度的最小单位,每个Pod中运行着一个或多个密切相关的业务容器,这些业务容器共享这个Pause容器的IP和Volume,我们以这个不易死亡的Pause容器作为Pod的根容器,以它的状态表示整个容器组的状态。一个Pod一旦被创建就会放到Etcd中存储,然后由Master调度到一个Node绑定,由这个Node上的Kubelet进行实例化。 每个Pod会被分配一个单独的Pod IP,Pod IP + ContainerPort 组成了一个Endpoint。 1.4 Service K8s中一个Service相当于一个微服务的概念,一个Service对应后端多个Pod计算实例,使用LabelSelector将一类Pod都绑定到自己上来。一般还会需要一个Deployment或者RC来帮助这个Service来保证这个Service的服务能力和质量。 1.4.1 kube-proxy负载均衡 运行在每个Node上的kube-proxy其实就是一个智能的软件负载均衡器,它负载将发给Service的请求转发到后端对应的Pod,也就是说它负责会话保持和负责均衡。 1.4.2 Cluster IP 负载均衡的基础是负载均衡器要维护一个后端Endpoint列表,但是Pod的Endpoint会随着Pod的销毁和重建而改变,k8s使这个问题透明化。一旦Service被创建,就会立刻分配给它一个Cluster IP,在Service的整个生命周期内,这个Cluster IP不会改变。于是,服务发现的问题也解决了:只要用Service Name和Service Cluster IP做一个DNS域名映射就可以了。 1.4.3 DNS 从Kubernetes 1.3开始,DNS通过使用插件管理系统cluster add-on,成为了一个内建的自启动服务。Kubernetes DNS在Kubernetes集群上调度了一个DNS Pod和Service,并配置kubelet,使其告诉每个容器使用DNS Service的IP来解析DNS名称。 (1)Service 集群中定义的每个Service(包括DNS Service它自己)都被分配了一个DNS名称。默认的,Pod的DNS搜索列表中会包含Pod自己的命名空间和集群的默认域,下面我们用示例来解释以下。 假设有一个名为foo的Service,位于命名空间bar中。运行在bar命名空间中的Pod可以通过DNS查找foo关键字来查找到这个服务,而运行在命名空间quux中的Pod可以通过关键字foo.bar来查找到这个服务。 普通(非headless)的Service都被分配了一个DNS记录,该记录的名称格式为my-svc.my-namespace.svc.cluster.local,通过该记录可以解析出服务的集群IP。 Headless(没有集群IP)的Service也被分配了一个DNS记录,名称格式为my-svc.my-namespace.svc.cluster.local。与普通Service不同的是,它会解析出Service选择的Pod的IP列表。 (2)Pod Pod也可以使用DNS服务。pod会被分配一个DNS记录,名称格式为pod-ip-address.my-namespace.pod.cluster.local。 比如,一个pod,它的IP地址为1.2.3.4,命名空间为default,DNS名称为cluster.local,那么它的记录就是:1-2-3-4.default.pod.cluster.local。 当pod被创建时,它的hostname设置在Pod的metadata.name中。 在v1.2版本中,用户可以指定一个Pod注解,pod.beta.kubernetes.io/hostname,用于指定Pod的hostname。这个Pod注解,一旦被指定,就将优先于Pod的名称,成为pod的hostname。比如,一个Pod,其注解为pod.beta.kubernetes.io/hostname: my-pod-name,那么该Pod的hostname会被设置为my-pod-name。 v1.2中还引入了一个beta特性,用户指定Pod注解,pod.beta.kubernetes.io/subdomain,来指定Pod的subdomain。比如,一个Pod,其hostname注解设置为“foo”,subdomain注解为“bar”,命名空间为“my-namespace”,那么它最终的FQDN就是“foo.bar.my-namespace.svc.cluster.local”。 在v1.3版本中,PodSpec有了hostname和subdomain字段,用于指定Pod的hostname和subdomain。它的优先级则高于上面提到的pod.beta.kubernetes.io/hostname和pod.beta.kubernetes.io/subdomain。 1.4.4 外部访问Service的问题 先明确这样几个IP: Node IP:Node主机的IP,与它是否属于K8s无关。 Pod IP:是Dokcer Engine通过docker0网桥的IP地址段进行分配的,通常是一个虚拟的二层网络。k8s中一个Pod访问另一个Pod就是通过Pod IP。 Cluster IP:仅用于Service对象,属于k8s的内部IP,外界无法直接访问。 (1)NodePort 在Service的yaml中定义NodePort,k8s为集群中每个Node都增加对这个端口的监听,使用这种方式往往需要一个独立与k8s之外的负载均衡器作为流量的入口。 (2)使用External IP 运行Hello World应用程序的五个实例。 创建一个暴露外部IP地址的Service对象。 使用Service对象访问正在运行的应用程序。 使用deployment创建暴露的Service对象: ~ kubectl expose deployment hello-world --type=LoadBalancer --name=my-service 显示关于Service的信息: ~ kubectl get services my-service NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE my-service 10.3.245.137 104.198.205.71 8080/TCP 54s ~ kubectl describe services my-service Name: my-service Namespace: default Labels: run=load-balancer-example Selector: run=load-balancer-example Type: LoadBalancer IP: 10.3.245.137 LoadBalancer Ingress: 104.198.205.71 Port: <unset> 8080/TCP NodePort: <unset> 32377/TCP Endpoints: 10.0.0.6:8080,10.0.1.6:8080,10.0.1.7:8080 + 2 more... Session Affinity: None Events: 在此例子中,外部IP地址为104.198.205.71。还要注意Port的值。在这个例子中,端口是8080。在上面的输出中,您可以看到该服务有多个端点:10.0.0.6:8080,10.0.1.6:8080,10.0.1.7:8080 + 2 more…。这些是运行Hello World应用程序的pod的内部地址。 使用外部IP地址访问Hello World应用程序: ~ curl http://<external-ip>:<port> Hello Kubernetes! 删除服务 ~ kubectl delete services my-service ~ kubectl delete deployment hello-world 1.5 Ingress 通常情况下,service和pod仅可在集群内部网络中通过IP地址访问。所有到达边界路由器的流量或被丢弃或被转发到其他地方。Ingress是授权入站连接到达集群服务的规则集合。你可以给Ingress配置提供外部可访问的URL、负载均衡、SSL、基于名称的虚拟主机等。用户通过POST Ingress资源到API server的方式来请求ingress。 Ingress controller负责实现Ingress,通常使用负载平衡器,它还可以配置边界路由和其他前端,这有助于以HA方式处理流量。 最简化的Ingress配置: apiVersion: extensions/v1beta1 kind: Ingress metadata: name: test-ingress spec: rules: - http: paths: - path: /testpath backend: serviceName: test servicePort: 80 - path: /bar backend: serviceName: s2 servicePort: 80 1-4行:跟Kubernetes的其他配置一样,ingress的配置也需要apiVersion,kind和metadata字段。配置文件的详细说明请查看部署应用, 配置容器和 使用resources. 5-7行: Ingress spec 中包含配置一个loadbalancer或proxy server的所有信息。最重要的是,它包含了一个匹配所有入站请求的规则列表。目前ingress只支持http规则。 8-9行:每条http规则包含以下信息:一个host配置项(比如for.bar.com,在这个例子中默认是*),path列表(比如:/testpath),每个path都关联一个backend(比如test:80)。在loadbalancer将流量转发到backend之前,所有的入站请求都要先匹配host和path。 10-12行:backend是一个service:port的组合。Ingress的流量被转发到它所匹配的backend。 配置TLS证书 你可以通过指定包含TLS私钥和证书的secret来加密Ingress。 目前,Ingress仅支持单个TLS端口443,并假定TLS termination。 如果Ingress中的TLS配置部分指定了不同的主机,则它们将根据通过SNI TLS扩展指定的主机名(假如Ingress controller支持SNI)在多个相同端口上进行复用。 TLS secret中必须包含名为tls.crt和tls.key的密钥,这里面包含了用于TLS的证书和私钥,例如: (1)创建Secret apiVersion: v1 data: tls.crt: base64 encoded cert tls.key: base64 encoded key kind: Secret metadata: name: testsecret namespace: default type: Opaque (2)创建Ingress: apiVersion: extensions/v1beta1 kind: Ingress metadata: name: no-rules-map spec: tls: - secretName: testsecret backend: serviceName: s1 servicePort: 80 2 高可用 Kubernetes服务本身的稳定运行对集群管理至关重要,影响服务稳定的因素一般来说分为两种,一种是服务本身异常或者服务所在机器宕机,另一种是因为网络问题导致的服务不可用。本文将从存储层、管理层、接入层三个方面介绍高可用Kubernetes集群的原理。 2.1 Etcd高可用方案 Kubernetes的存储层使用的是Etcd。Etcd是CoreOS开源的一个高可用强一致性的分布式存储服务,Kubernetes使用Etcd作为数据存储后端,把需要记录的pod、rc、service等资源信息存储在Etcd中。 Etcd使用raft算法将一组主机组成集群,raft 集群中的每个节点都可以根据集群运行的情况在三种状态间切换:follower, candidate 与 leader。leader 和 follower 之间保持心跳。如果follower在一段时间内没有收到来自leader的心跳,就会转为candidate,发出新的选主请求。 集群初始化的时候内部的节点都是follower节点,之后会有一个节点因为没有收到leader的心跳转为candidate节点,发起选主请求。当这个节点获得了大于一半节点的投票后会转为leader节点。当leader节点服务异常后,其中的某个follower节点因为没有收到leader的心跳转为candidate节点,发起选主请求。只要集群中剩余的正常节点数目大于集群内主机数目的一半,Etcd集群就可以正常对外提供服务。 当集群内部的网络出现故障集群可能会出现“脑裂”问题,这个时候集群会分为一大一小两个集群(奇数节点的集群),较小的集群会处于异常状态,较大的集群可以正常对外提供服务。 2.2 Master高可用方案 Master上有三个关键的服务:apiserver、controller-manager和scheduler,这三个不一定要运行在一台主机上。 2.2.1 controller-manager和scheduler的选举配置 Kubernetes的管理层服务包括kube-scheduler和kube-controller-manager。kube-scheduer和kube-controller-manager使用一主多从的高可用方案,在同一时刻只允许一个服务处以具体的任务。Kubernetes中实现了一套简单的选主逻辑,依赖Etcd实现scheduler和controller-manager的选主功能。 如果scheduler和controller-manager在启动的时候设置了leader-elect参数,它们在启动后会先尝试获取leader节点身份,只有在获取leader节点身份后才可以执行具体的业务逻辑。它们分别会在Etcd中创建kube-scheduler和kube-controller-manager的endpoint,endpoint的信息中记录了当前的leader节点信息,以及记录的上次更新时间。leader节点会定期更新endpoint的信息,维护自己的leader身份。每个从节点的服务都会定期检查endpoint的信息,如果endpoint的信息在时间范围内没有更新,它们会尝试更新自己为leader节点 scheduler服务以及controller-manager服务之间不会进行通信,利用Etcd的强一致性,能够保证在分布式高并发情况下leader节点的全局唯一性。整体方案如下图所示: 当集群中的leader节点服务异常后,其它节点的服务会尝试更新自身为leader节点,当有多个节点同时更新endpoint时,由Etcd保证只有一个服务的更新请求能够成功。通过这种机制sheduler和controller-manager可以保证在leader节点宕机后其它的节点可以顺利选主,保证服务故障后快速恢复。当集群中的网络出现故障时对服务的选主影响不是很大,因为scheduler和controller-manager是依赖Etcd进行选主的,在网络故障后,可以和Etcd通信的主机依然可以按照之前的逻辑进行选主,就算集群被切分,Etcd也可以保证同一时刻只有一个节点的服务处于leader状态。 2.2.2 apiserver的高可用 Kubernetes的接入层服务主要是kube-apiserver。apiserver本身是无状态的服务,它的主要任务职责是把资源数据存储到Etcd中,后续具体的业务逻辑是由scheduler和controller-manager执行的。所以可以同时起多个apiserver服务,使用nginx把客户端的流量转发到不同的后端apiserver上实现接入层的高可用。具体的实现如下图所示: 接入层的高可用分为两个部分,一个部分是多活的apiserver服务,另一个部分是一主一备的nginx服务。 2.3 Keepalived简介 Keepalived软件起初是专为LVS负载均衡软件设计的,用来管理并监控LVS集群系统中各个服务节点的状态,后来又加入了可以实现高可用的VRRP功能。因此,Keepalived除了能够管理LVS软件外,还可以作为其他服务(例如:Nginx、Haproxy、MySQL等)的高可用解决方案软件。Keepalived软件主要是通过VRRP协议实现高可用功能的。VRRP是Virtual Router RedundancyProtocol(虚拟路由器冗余协议)的缩写,VRRP出现的目的就是为了解决静态路由单点故障问题的,它能够保证当个别节点宕机时,整个网络可以不间断地运行。所以,Keepalived 一方面具有配置管理LVS的功能,同时还具有对LVS下面节点进行健康检查的功能,另一方面也可实现系统网络服务的高可用功能。 故障切换转移原理 Keepalived高可用服务对之间的故障切换转移,是通过 VRRP (Virtual Router Redundancy Protocol ,虚拟路由器冗余协议)来实现的。在 Keepalived服务正常工作时,主 Master节点会不断地向备节点发送(多播的方式)心跳消息,用以告诉备Backup节点自己还活看,当主 Master节点发生故障时,就无法发送心跳消息,备节点也就因此无法继续检测到来自主 Master节点的心跳了,于是调用自身的接管程序,接管主Master节点的 IP资源及服务。而当主 Master节点恢复时,备Backup节点又会释放主节点故障时自身接管的IP资源及服务,恢复到原来的备用角色。 3 容器网络 3.1 docker默认容器网络 在默认情况下会看到三个网络,它们是Docker Deamon进程创建的。它们实际上分别对应了Docker过去的三种『网络模式』,可以使用docker network ls来查看: master@ubuntu:~$ sudo docker network ls NETWORK ID NAME DRIVER SCOPE 18d934794c74 bridge bridge local f7a7b763f013 host host local 697354257ae3 none null local 这 3 个网络包含在 Docker 实现中。运行一个容器时,可以使用 the –net标志指定您希望在哪个网络上运行该容器。您仍然可以使用这 3 个网络。 bridge 网络表示所有 Docker 安装中都存在的 docker0 网络。除非使用 docker run –net=选项另行指定,否则 Docker 守护进程默认情况下会将容器连接到此网络。在主机上使用 ifconfig命令,可以看到此网桥是主机的网络堆栈的一部分。 none 网络在一个特定于容器的网络堆栈上添加了一个容器。该容器缺少网络接口。 host 网络在主机网络堆栈上添加一个容器。您可以发现,容器中的网络配置与主机相同。 3.2 跨主机通信的方案 和host共享network namespace 这种接入模式下,不会为容器创建网络协议栈,即容器没有独立于host的network namespace,但是容器的其他namespace(如IPC、PID、Mount等)还是和host的namespace独立的。容器中的进程处于host的网络环境中,与host共用L2-L4的网络资源。该方式的优点是,容器能够直接使用host的网络资源与外界进行通信,没有额外的开销(如NAT),缺点是网络的隔离性差,容器和host所使用的端口号经常会发生冲突。 和host共享物理网卡 2与1的区别在于,容器和host共享物理网卡,但容器拥有独立于host的network namespace,容器有自己的MAC地址、IP地址、端口号。这种接入方式主要使用SR-IOV技术,每个容器被分配一个VF,直接通过PCIe网卡与外界通信,优点是旁路了host kernel不占任何计算资源,而且IO速度较快,缺点是VF数量有限且对容器迁移的支持不足。 Behind the POD 这种方式是Google在Kubernetes中的设计中提出来的。Kubernetes中,POD是指一个可以被创建、销毁、调度、管理的最小的部署单元,一个POD有一个基础容器以及一个或一组应用容器,基础容器对应一个独立的network namespace并拥有一个其它POD可见的IP地址(以IP A.B.C.D指代),应用容器间则共享基础容器的network namespace(包括MAC、IP以及端口号等),还可以共享基础容器的其它的namespace(如IPC、PID、Mount等)。POD作为一个整体连接在host的vbridge/vswitch上,使用IP地址A.B.C.D与其它POD进行通信,不同host中的POD处于不同的subnet中,同一host中的不同POD处于同一subnet中。这种方式的优点是一些业务上密切相关的容器可以共享POD的全部资源(它们一般不会产生资源上的冲突),而这些容器间的通信高效便利。 3.3 Flannel 在k8s的网络设计中,服务以POD为单位,每个POD的IP地址,容器通过Behind the POD方式接入网络(见“容器的网络模型”),一个POD中可包含多个容器,这些容器共享该POD的IP地址。另外,k8s要求容器的IP地址都是全网可路由的,那么显然docker0+iptables的NAT方案是不可行的。 实现上述要求其实有很多种组网方法,Flat L3是一种(如Calico),Hierarchy L3(如Romana)是一种,另外L3 Overlay也是可以的,CoreOS就采用L3 Overlay的方式设计了flannel, 并规定每个host下各个POD属于同一个subnet,不同的host/VM下的POD属于不同subnet。我们来看flannel的架构,控制平面上host本地的flanneld负责从远端的ETCD集群同步本地和其它host上的subnet信息,并为POD分配IP地址。数据平面flannel通过UDP封装来实现L3 Overlay,既可以选择一般的TUN设备又可以选择VxLAN设备(注意,由于图来源不同,请忽略具体的IP地址)。 flannel是CoreOS提供用于解决Dokcer集群跨主机通讯的覆盖网络工具。它的主要思路是:预先留出一个网段,每个主机使用其中一部分,然后每个容器被分配不同的ip;让所有的容器认为大家在同一个直连的网络,底层通过UDP/VxLAN等进行报文的封装和转发。 flannel默认使用8285端口作为UDP封装报文的端口,VxLan使用8472端口。那么一条网络报文是怎么从一个容器发送到另外一个容器的呢? 容器直接使用目标容器的ip访问,默认通过容器内部的eth0发送出去。 报文通过veth pair被发送到vethXXX。 vethXXX是直接连接到虚拟交换机docker0的,报文通过虚拟bridge docker0发送出去。 查找路由表,外部容器ip的报文都会转发到flannel0虚拟网卡,这是一个P2P的虚拟网卡,然后报文就被转发到监听在另一端的flanneld。 flanneld通过etcd维护了各个节点之间的路由表,把原来的报文UDP封装一层,通过配置的iface发送出去。 报文通过主机之间的网络找到目标主机。 报文继续往上,到传输层,交给监听在8285端口的flanneld程序处理。 数据被解包,然后发送给flannel0虚拟网卡。 查找路由表,发现对应容器的报文要交给docker0。 docker0找到连到自己的容器,把报文发送过去。 作者:奋起直追CDS 原文:https://blog.csdn.net/Dustin_CDS/article/details/79439596

优秀的个人博客,低调大师

Unity ShaderLab学习总结

本文只讨论Unity ShaderLab相关的知识和使用方法。但, 既不讨论渲染相关的基础概念,基础概念可参考Rendering Pipeline Overview等文章。也不讨论具体的渲染技巧移动设备GPU和桌面设备GPU硬件架构上有较多不同点,详见下面的“移动设备GPU架构简述”一章。一句话总结: GameObject里有MeshRenderer,MeshRenderer里有Material列表,每个Material里有且只有一个Shader;Material在编辑器暴露该Shader的可调属性。所以关键是怎么编写Shader。 Shader基础编辑器使用MonoDevelop这反人类的IDE来编写Shader居然是让人满意的。有语法高亮,无语法提示。如果习惯VisualStudio,可以如下实现.Shader文件的语法高亮。 下载作者donaldwu自己添加的关键词文件usertype.dat。其包括了Unity ShaderLab的部分关键字,和HLSL的所有关键字。关键字以后持续添加中。将下载的usertype.dat放到Microsoft Visual Studio xx.xCommonXIDE文件夹下;打开VS,工具>选项>文本编辑器>文件扩展名,扩展名里填“shader”,编辑器选VC++,点击添加;重启VS,Done。ShaderShader "ShaderLab Tutorials/TestShader"{// ...}Shader的名字会直接决定shader在material里出现的路径 SubShaderShader "ShaderLab Tutorials/TestShader" {SubShader{//...}}一个Shader有多个SubShader。一个SubShader可理解为一个Shader的一个渲染方案。即SubShader是为了针对不同的渲染情况而编写的。每个Shader至少1个SubShader、理论可以无限多个,但往往两三个就足够。一个时刻只会选取一个SubShader进行渲染,具体SubShader的选取规则包括: 从上到下选取SubShader的标签、Pass的标签是否符合当前的“Unity渲染路径”是否符合当前的ReplacementTagSubShader是否和当前的GPU兼容等按此规则第一个被选取的SubShader将会用于渲染,未被选取的SubShader在这次渲染将被忽略。 SubShader的TagShader "ShaderLab Tutorials/TestShader" {SubShader{Tags { "Queue"="Geometry+10" "RenderType"="Opaque" }//...}}SubShader内部可以有标签(Tags)的定义。Tag指定了这个SubShader的渲染顺序(时机),以及其他的一些设置。 "RenderType"标签。Unity可以运行时替换符合特定RenderType的所有Shader。Camera.RenderWithShader或Camera.SetReplacementShader配合使用。Unity内置的RenderType包括:"Opaque":绝大部分不透明的物体都使用这个;"Transparent":绝大部分透明的物体、包括粒子特效都使用这个;"Background":天空盒都使用这个;"Overlay":GUI、镜头光晕都使用这个;用户也可以定义任意自己的RenderType这个标签所取的值。应注意,Camera.RenderWithShader或Camera.SetReplacementShader不要求标签只能是RenderType,RenderType只是Unity内部用于Replace的一个标签而已,你也可以自定义自己全新的标签用于Replace。比如,你为自己的ShaderA.SubShaderA1(会被Unity选取到的SubShader,常为Shader文件中的第一个SubShader)增加Tag为"Distort"="On",然后将"Distort"作为参数replacementTag传给函数。此时,作为replacementShader实参的ShaderB.SubShaderB1中若有也有一模一样的"Distort"="On"、且这A的材质参数包含B所需材质参数,则此SubShaderB1将代替SubShaderA1用于本次渲染。具体可参考Rendering with Replaced Shaders"Queue"标签。定义渲染顺序。预制的值为"Background"。值为1000。比如用于天空盒。"Geometry"。值为2000。大部分物体在这个队列。不透明的物体也在这里。这个队列内部的物体的渲染顺序会有进一步的优化(应该是从近到远,early-z test可以剔除不需经过FS处理的片元)。其他队列的物体都是按空间位置的从远到近进行渲染。"AlphaTest"。值为2450。已进行AlphaTest的物体在这个队列。"Transparent"。值为3000。透明物体。"Overlay"。值为4000。比如镜头光晕。用户可以定义任意值,比如"Queue"="Geometry+10""ForceNoShadowCasting",值为"true"时,表示不接受阴影。"IgnoreProjector",值为"true"时,表示不接受Projector组件的投影。

优秀的个人博客,低调大师

Docker学习之路(三)

解决方法: yum install device-mapper-event-libs 然后重启docker服务 service docker restart 注意:此命令需要操作两次,第一次会重启失败 docker已经运行镜像删除方法 停止所有的container,这样才能够删除其中的images: docker stop $(docker ps -a -q) 如果想要删除所有container的话再加一个指令: docker rm $(docker ps -a -q) 查看当前有些什么images docker images 删除images,通过image的id来指定删除谁 docker rmi <image id> 想要删除untagged images,也就是那些id为的image的话可以用 docker rmi $(docker images | grep "^<none>" | awk "{print $3}") 要删除全部image的话 docker rmi $(docker images -q) Dockerfile制作镜像实例 下载jdk、tomcat安装包,上传/usr/local/soft目录下 jdk1.8下载:http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html tomcat8下载:https://tomcat.apache.org/download-80.cgi 1. tar -zxvf apache-tomcat-8.5.38.tar.gz #解压tomcat 2. tar -zxvf jdk-8u201-linux-x64.tar.gz #解压jdk 3. rm -rf apache-tomcat-8.5.31.tar.gz #删除安装包 4. rm -rf jdk-8u171-linux-x64.tar.gz #删除安装包 5. touch Dockerfile #创建文件 一系列操作完成后 soft文件夹内容接下来编写dockerfile文件 #指定操作的镜像 FROM centos # 维护者信息 MAINTAINER shuai #将jdk1.8.0_171添加到镜像centos的/usr/local/soft/目录下,并命名为jdk ADD jdk1.8.0_201 /usr/local/soft/jdk #将apache-tomcat-8.5.31添加到镜像centos的/usr/local/soft/目录下,并命名为tomcat ADD apache-tomcat-8.5.38 /usr/local/soft/tomcat #添加环境变量 ENV JAVA_HOME /usr/local/soft/jdk ENV CATALINA_HOME /usr/local/soft/tomcat ENV PATH $PATH:$JAVA_HOME/bin:$CATALINA_HOME/bin #暴露8080端口 EXPOSE 8080 #启动时运行tomcat CMD ["/usr/local/soft/tomcat/bin/catalina.sh","run"] FROM : 指定基础镜像,并且必须是第一条指令 重点:此命令不可省略,然后如果docker容器中没有centos镜像,此操作会默认产生一个centos镜像 MAINTAINER : 指定作者 RUN : 运行指定的命令 ADD : 复制命令,把文件复制到镜像中。 ENV : 设置环境变量 EXPOSE : 功能为暴漏容器运行时的监听端口给外部 CMD : 指定容器启动时运行的命令 构建Docker镜像 docker build -t repostory/centos_tomcat . 注意:后面的点不要省略-t 设置tag名称, 命名规则registry/image:tag(若不添加版本号,默认latest). 表示使用当前目录下的Dockerfile文件(注意语句后面有一个点) 启动镜像访问 docker run -d -p 8080:8080 --name Icentos repostory/centos_tomcat -d 后台运行 -p 端口映射 宿主机port : 容器port --name 指定容器运行名称 接下来在浏览器中输入地址查看成功与否出现此界面就大功告成啦!

优秀的个人博客,低调大师

Docker学习之路(二)

Docker独立IP及容器关联 内置 brldge (nat) 缺点:需要配套服务注册/发现,否则宿主上端口分配困难,容易冲突。由于每个容器暴露的端口都不一致,造成前端路由层nginx配置(peoxy_pass)里无法使用dns的方式。端口映射要在容器启动时就指定好,后期无法变更。nat不支持websocket。 自建桥接网络 优点:每个容器都有独立ip,对外提供服务,如nginx+php,都可以使用默认的80端口由于容器暴露端口都可以使用80端口,因此前端路由层nginx配置(proxy_pass)里可以使用dns的方式。无需为了后期端口映射添加而烦恼桥接支持websocket ---------以下操作是基于Centos6.5版本系统,Centos7以上没做实验 首先 关闭docker服务 service docker stop 删除docker0的网卡 ifconfig docker0 down brctl delbr docker0 进入网卡配置cd /etc/sysconfig/network-scripts/vi ifcfg-eth0 vi ifcfg-br0然后重启一下服务 /etc/init.d/network restart然后ping一下百度 试试成功与否然后 vi /etc/sysconfig/docker //修改docker为桥接模式然后 /etc/init.d/docker restart 注意操作两次 然后启动一个容器docker run -i -d -t centos /bin/bash然后 ifconfig查看 如果出现下图表示成功然后 docker ps查看容器id 然后 docker attach b66317f0d73b进入容器 然后查看ip,如果和主机ip相同网段表示成功!

优秀的个人博客,低调大师

Docker学习之路(一)

什么是Docker Docker是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器上,也可以实现虚拟化。Docker项目的目标是实现轻量级的操作系统虚拟化解决方案,Docker的基础是Linux容器(LXC)、Cgroup等技术 docker和传统虚拟化的区别 Docker和传统虚拟化(KVM、XEN)方式的不同之处是容器是在操作系统层面上实现虚拟化,直接复用本地主机的操作系统,而传统方式则是在硬件的基础上,虚拟出自己的系统,再在系统上部署相关的APP应用。 重点:Docker虚拟化有三个概念需要理解,分别镜像、容器、仓库。 1、镜像:docker的镜像其实就是模板,跟我们常见的ISO镜像类似,是一个模板。 2、仓库:仓库是存放镜像的地方,分为公开仓库(public)和私有仓库(private)两种形式 3、容器:容器是完全使用沙箱机制,相互之间不会有任何接口。几乎没有性能开销,可以很容易地在机器和数据中心中运行。最重要的是,他们不依赖于任何语言、框架或包括系统。 Docker虚拟化的安装 center7以上机器才能使用yum直接安装,如果其他版本需要安装centos扩展源epel。centos6.x系列安装Docker软件,首先要关闭selinux,然后需要安装相应的epel源,如下: 首先关闭selinux vi /etc/selinux/config 修改文件内容 ELINUX=disabled 然后命令行sestatus检查状态下载扩展包 wget http://download.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm 安装 rpm -ivh epel-release-6-8.noarch.rpm安装容器和资源限制 yum install lxc libcgroup安装docker yum install docker-io 注意:如果上面命令安装失败,可以用下面命令下载 yum install https://get.docker.com/rpm/1.7.1/centos-6/RPMS/x86_64/docker-engine-1.7.1-1.el6.x86_64.rpm 启动docker进程 /etc/init.d/docker start查看docker进程 ps -ef|grep docker下载tomcat镜像 docker pull tomcat 操作命令 查看镜像 docker images删除一个镜像 docker rm 镜像名 保存一个镜像 docker save 镜像名 > centos.jar导入一个镜像 docker load < nginx1.11.tar 导入容器 docker import - centos7导出容器 docker export i容器d >centos7.tar查看docker版本 docker version查看docker服务启动 ps -ef |grep docker查看容器状态 docker ps -l搜索可用docker镜像 docker search centos运行镜像 docker run centos关闭容器 docker stop id 启动某个容器 docker start id进入一个容器 docker attach id删除一个容器 docker rm id删除一个镜像 docker rmi images退出一个容器 exit 正常退出不关闭Ctrl+P+Q容器中安装ntpdate `docker run centos yum install ntpdate docker run -i -t centos(容器名称) /bin/bash 在容器里启动一个/bin/bash shell环境,可以登录进入操作,其中-t表示打开一个终端的意思,-i表示可以交互输入查看系统版本 cat /etc/redhat-release * 7.0以上系统的命令

优秀的个人博客,低调大师

spring-boot学习

Controller的使用 注: 通常程序开发后端需要提供的是rest接口,返回一些json格式给前端,尽量不要使用模板的方式,使用模板会在性能上带来很大的损耗. 参数的获取 知识点整理 @Restcontroller和@controller的区别 如果使用@Restcontroller注解,就不能返回jsp,html页面,视图解析器无法解析jsp,html页面 @Controller注解,若返回json等内容到页面,则需要加@Response注解,而且需要页面返回 url映射可以是以集合的形式获取 例:@RequestMapping(value ={"/hello/","/hi"}) 获取一个id值 :@RequestMapping(value ={"/hello/{id}",method=RequestMethod.GET}) @PathVariable("id") 获取操作 导航栏: /hello/12也可以 12/hello @RequestParam("id") 获取操作 导航栏

优秀的个人博客,低调大师

Django学习笔记(一)

image.png 最近有个需求,老大让用 Django 来做,以前入门 Python 时就听过 Django 的大名,今日一见果然名不虚传~~~~。 特点 Django 最大的特点就是快速建站: 快速开发 内置应用 后台admin 用户认证系统auth 会话系统sessions 安全性高 表单验证 SQL注入 跨站点攻击 易于拓展 ....很多,这里不一一列举。 Django 应用(app)的概念 项目VS应用 项目与应用之间有什么不同之处?应用是一个提供功能的 Web 应用 – 例如:一个博客系统、一个公共记录的数据库或者一个简单的投票系统。 项目是针对一个特定的 Web 网站相关的配置和其应用的组合。一个项目可以包含多个应用。一个应用可以在多个项目中使用。 Django使用应用来分割功能,也就是app,每个应用分别为不同的app。 例如:我们创建一个电商网站,那么里边的购物车、用户管理、支付系统都可以成为独立的模块,也就是独立的三个app,这些模块可以用在别的网站中,不单单只针对于当前网站。 Django Demo Python版本2.7 Django版本1.8 查看已安装Django的版本:python -c "import django;print(django.get_version())" 创建一个名为web_Demo的Django项目 命令:django-admin startproject web_Demo 完成后,查看目录结构: image.png manage.py : 一个实用的命令行工具,可以让你以各种方式与该Django项目交互。可以在django-admin.py和manage.py查看源码的细节。 settings.py : 该项目的配置文件。 urls.py : 该项目的URL生明。 wsgi.py : 一个WSGI兼容的Web服务器入口。 接着,创建一个名为blog的应用(app):python manage.py startapp blog 查看新的目录结构: image.png 应用模块中各文件的作用: migrations:数据迁移模块 admin.py : 该应用的后台管理系统配置文件 apps.py : 当前应用的一些配置,1.9版本后才会自动生成,1.8 1.7都不会自动生成 models.py : 数据模型 使用ORM框架 django已经有所封装 tests.py : 自动化测试模块 views.py : 执行响应的逻辑代码,代码逻辑处理的主要地点,项目中大部分代码都在这里编写 然后,把blog app添加到配置文件中: 编辑settings.py: INSTALLED_APPS = ( 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'blog' ) 解释一下上边的配置信息代表什么意思: admin :身份验证系统 auth : contenttypes:内容类型框架 sessions :session框架 messages :消息框架 staticfiles :静态文件框架 再执行命令python manage.py migrate image.png 完成。 Admin Admin是Django自带的一个功能强大的自动化数据管理界面,被授权的用户可直接在admin中管理数据库。 Django提供了许多针对Admin的定制功能。 首先,创建超级用户查看admin系统:python manage.py createsuperuser 输入用户名、邮箱、密码,这里密码要难一些,简单的密码可能会不行。 image.png 启动web项目:python manage.py runserver 这里默认8000端口, 浏览器中打开127.0.0.1/8000/admin 输入刚才设置的用户名密码: image.png 登录: image.png 可以看到admin界面。 这里显示的是英文的admin,我们可以通过配置改为中文的。 更改settings中的配置: LANGUAGE_CODE = 'zh-Hans' image.png 完成。

资源下载

更多资源
优质分享App

优质分享App

近一个月的开发和优化,本站点的第一个app全新上线。该app采用极致压缩,本体才4.36MB。系统里面做了大量数据访问、缓存优化。方便用户在手机上查看文章。后续会推出HarmonyOS的适配版本。

Oracle

Oracle

Oracle Database,又名Oracle RDBMS,或简称Oracle。是甲骨文公司的一款关系数据库管理系统。它是在数据库领域一直处于领先地位的产品。可以说Oracle数据库系统是目前世界上流行的关系数据库管理系统,系统可移植性好、使用方便、功能强,适用于各类大、中、小、微机环境。它是一种高效率、可靠性好的、适应高吞吐量的数据库方案。

Eclipse

Eclipse

Eclipse 是一个开放源代码的、基于Java的可扩展开发平台。就其本身而言,它只是一个框架和一组服务,用于通过插件组件构建开发环境。幸运的是,Eclipse 附带了一个标准的插件集,包括Java开发工具(Java Development Kit,JDK)。

JDK

JDK

JDK是 Java 语言的软件开发工具包,主要用于移动设备、嵌入式设备上的java应用程序。JDK是整个java开发的核心,它包含了JAVA的运行环境(JVM+Java系统类库)和JAVA工具。