This commit is contained in:
周伟
2022-05-11 22:56:52 +08:00
parent d9c5ffd627
commit 85b6063789
2559 changed files with 545 additions and 30 deletions

View File

@@ -12,7 +12,9 @@
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
@@ -25,61 +27,86 @@
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a class="current-tab" href="/极客时间/Java基础36讲.md.html">Java基础36讲.md.html</a>
</li>
<li>
<a href="/极客时间/Java错误示例100讲.md.html">Java错误示例100讲.md.html</a>
</li>
<li>
<a href="/极客时间/Linux性能优化.md.html">Linux性能优化.md.html</a>
</li>
<li>
<a href="/极客时间/MySQL实战45讲.md.html">MySQL实战45讲.md.html</a>
</li>
<li>
<a href="/极客时间/从0开始学微服务.md.html">从0开始学微服务.md.html</a>
</li>
<li>
<a href="/极客时间/代码精进之路.md.html">代码精进之路.md.html</a>
</li>
<li>
<a href="/极客时间/持续交付36讲.md.html">持续交付36讲.md.html</a>
</li>
<li>
<a href="/极客时间/程序员进阶攻略.md.html">程序员进阶攻略.md.html</a>
</li>
<li>
<a href="/极客时间/趣谈网络协议.md.html">趣谈网络协议.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
@@ -94,6 +121,8 @@
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
@@ -106,7 +135,9 @@ function hide_canvas() {
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
@@ -186,6 +217,7 @@ jaotc --output libjava.base.so --module java.base
</code></pre>
<p>然后,在启动时直接指定就可以了。</p>
<pre><code>java -XX:AOTLibrary=./libHelloWorld.so,./libjava.base.so HelloWorld
</code></pre>
<p>而且Oracle JDK 支持分层编译和 AOT 协作使用,这两者并不是二选一的关系。如果你有兴趣,可以参考相关文档:<a href="https://openjdk.java.net/jeps/295">http://openjdk.java.net/jeps/295</a>。AOT 也不仅仅是只有这一种方式,业界早就有第三方工具(如 GCJ、Excelsior JET提供相关功能。</p>
<p>另外JVM 作为一个强大的平台,不仅仅只有 Java 语言可以运行在 JVM 上本质上合规的字节码都可以运行Java 语言自身也为此提供了便利,我们可以看到类似 Clojure、Scala、Groovy、JRuby、Jython 等大量 JVM 语言,活跃在不同的场景。</p>
@@ -569,9 +601,11 @@ ${JAVA_HOME}/bin/javap -v StringConcat.class
<p>看起来很不错是吧?但实际情况估计会让你大跌眼镜。一般使用 Java 6 这种历史版本,并不推荐大量使用 intern为什么呢魔鬼存在于细节中被缓存的字符串是存在所谓 PermGen 里的,也就是臭名昭著的“永久代”,这个空间是很有限的,也基本不会被 FullGC 之外的垃圾收集照顾到。所以如果使用不当OOM 就会光顾。</p>
<p>在后续版本中,这个缓存被放置在堆中,这样就极大避免了永久代占满的问题,甚至永久代在 JDK 8 中被 MetaSpace元数据区替代了。而且默认缓存大小也在不断地扩大中从最初的 1009到 7u40 以后被修改为 60013。你可以使用下面的参数直接打印具体数字可以拿自己的 JDK 立刻试验一下。</p>
<pre><code>-XX:+PrintStringTableStatistics
</code></pre>
<p>你也可以使用下面的 JVM 参数手动调整大小,但是绝大部分情况下并不需要调整,除非你确定它的大小已经影响了操作效率。</p>
<pre><code>-XX:StringTableSize=N
</code></pre>
<p>Intern 是一种<strong>显式地排重机制</strong>,但是它也有一定的副作用,因为需要开发者写代码时明确调用,一是不方便,每一个都显式调用是非常麻烦的;另外就是我们很难保证效率,应用开发阶段很难清楚地预计字符串的重复情况,有人认为这是一种污染代码的实践。</p>
<p>幸好在 Oracle JDK 8u20 之后,推出了一个新的特性,也就是 G1 GC 下的字符串排重。它是通过将相同数据的字符串指向同一份数据来做到的,是 JVM 底层的改变,并不需要 Java 类库做什么修改。</p>
@@ -630,6 +664,7 @@ ${JAVA_HOME}/bin/javap -v StringConcat.class
</code></pre>
<p>因为反射机制使用广泛根据社区讨论目前Java 9 仍然保留了兼容 Java 8 的行为,但是很有可能在未来版本,完全启用前面提到的针对 setAccessible 的限制,即只有当被反射操作的模块和指定的包对反射调用者模块 Open才能使用 setAccessible我们可以使用下面参数显式设置。</p>
<pre><code>--illegal-access={ permit | warn | deny }
</code></pre>
<p>\2. 动态代理</p>
<p>前面的问题问到了动态代理,我们一起看看,它到底是解决什么问题?</p>
@@ -759,6 +794,7 @@ java/lang/Integer.intValue:()I
<p>首先继续深挖缓存Integer 的缓存范围虽然默认是 -128 到 127但是在特别的应用场景比如我们明确知道应用会频繁使用更大的数值这时候应该怎么办呢</p>
<p>缓存上限值实际是可以根据需要调整的JVM 提供了参数设置:</p>
<pre><code>-XX:AutoBoxCacheMax=N
</code></pre>
<p>这些实现,都体现在<a href="http://hg.openjdk.java.net/jdk/jdk/file/26ac622a4cab/src/java.base/share/classes/java/lang/Integer.java">java.lang.Integer</a>源码之中,并实现在 IntegerCache 的静态初始化块里。</p>
<pre><code>private static class IntegerCache {
@@ -855,9 +891,11 @@ java/lang/Integer.intValue:()I
</ul>
<p>我今天介绍的这些集合类,都不是线程安全的,对于 java.util.concurrent 里面的线程安全容器,我在专栏后面会去介绍。但是,并不代表这些集合完全不能支持并发编程的场景,在 Collections 工具类中,提供了一系列的 synchronized 方法,比如</p>
<pre><code>static &lt;T&gt; List&lt;T&gt; synchronizedList(List&lt;T&gt; list)
</code></pre>
<p>我们完全可以利用类似方法来实现基本的线程安全集合:</p>
<pre><code>List list = Collections.synchronizedList(new ArrayList());
</code></pre>
<p>它的实现,基本就是将每个基本方法,比如 get、set、add 之类,都通过 synchronizd 添加基本的同步支持,非常简单粗暴,但也非常实用。注意这些方法创建的线程安全集合,都符合迭代时 fail-fast 行为,当发生意外的并发修改时,尽早抛出 ConcurrentModificationException 异常,以避免不可预计的行为。</p>
<p>另外一个经常会被考察到的问题,就是理解 Java 提供的默认排序算法,具体是什么排序方式以及设计思路等。</p>
@@ -877,6 +915,7 @@ list.add(&quot;World&quot;);
</code></pre>
<p>而利用新的容器静态工厂方法,一句代码就够了,并且保证了不可变性。</p>
<pre><code>List&lt;String&gt; simpleList = List.of(&quot;Hello&quot;,&quot;world&quot;);
</code></pre>
<p>更进一步,通过各种 of 静态工厂方法创建的实例,还应用了一些我们所谓的最佳实践,比如,它是不可变的,符合我们对线程安全的需求;它因为不需要考虑扩容,所以空间上更加紧凑等。</p>
<p>如果我们去看 of 方法的源码,你还会发现一个特别有意思的地方:我们知道 Java 已经支持所谓的可变参数varargs但是官方类库还是提供了一系列特定参数长度的方法看起来似乎非常不优雅为什么呢这其实是为了最优的性能JVM 在处理变长参数的时候会有明显的额外开销,如果你需要实现性能敏感的 API也可以进行参考。</p>
@@ -1023,6 +1062,7 @@ public class LinkedHashMapSample {
<li>具体键值对在哈希表中的位置(数组 index取决于下面的位运算</li>
</ul>
<pre><code>i = (n - 1) &amp; hash
</code></pre>
<p>仔细观察哈希值的源头,我们会发现,它并不是 key 本身的 hashCode而是来自于 HashMap 内部的另外一个 hash 方法。注意,为什么这里需要将高位数据移位到低位进行异或运算呢?<strong>这是因为有些数据计算出的哈希值差异主要在高位,而 HashMap 里的哈希寻址是忽略容量以上的高位的,那么这种处理就可以有效避免类似情况下的哈希碰撞。</strong></p>
<pre><code>static final int hash(Object kye) {
@@ -1385,11 +1425,13 @@ public class LinkedHashMapSample {
<p>Selector 同样是基于底层操作系统机制,不同模式、不同版本都存在区别,例如,在最新的代码库里,相关实现如下:</p>
<p>Linux 上依赖于 epoll<a href="http://hg.openjdk.java.net/jdk/jdk/file/d8327f838b88/src/java.base/linux/classes/sun/nio/ch/EPollSelectorImpl.java">http://hg.openjdk.java.net/jdk/jdk/file/d8327f838b88/src/java.base/linux/classes/sun/nio/ch/EPollSelectorImpl.java</a>)。</p>
<pre><code>Windows 上 NIO2AIO模式则是依赖于 iocphttp://hg.openjdk.java.net/jdk/jdk/file/d8327f838b88/src/java.base/windows/classes/sun/nio/ch/Iocp.java
</code></pre>
<ul>
<li>Chartset提供 Unicode 字符串定义NIO 也提供了相应的编解码器等,例如,通过下面的方式进行字符串到 ByteBuffer 的转换:</li>
</ul>
<pre><code>Charset.defaultCharset().encode(&quot;Hello world!&quot;));
</code></pre>
<p>2.NIO 能解决什么问题?</p>
<p>下面我通过一个典型场景,来分析为什么需要 NIO为什么需要多路复用。设想我们需要实现一个服务器应用只简单要求能够同时服务多个客户端请求即可。</p>
@@ -1669,6 +1711,7 @@ throws IOException
<p>但是请注意Direct Buffer 创建和销毁过程中,都会比一般的堆内 Buffer 增加部分开销,所以通常都建议用于长期使用、数据较大的场景。</p>
<p>使用 Direct Buffer我们需要清楚它对内存和 JVM 参数的影响。首先,因为它不在堆上,所以 Xmx 之类参数,其实并不能影响 Direct Buffer 等堆外成员所使用的内存额度,我们可以使用下面参数设置大小:</p>
<pre><code>-XX:MaxDirectMemorySize=512M
</code></pre>
<p>从参数设置和内存问题排查角度来看,这意味着我们在计算 Java 可以使用的内存大小的时候,不能只考虑堆的需要,还有 Direct Buffer 等一系列堆外因素。如果出现内存不足,堆外内存占用也是一种可能性。</p>
<p>另外,大多数垃圾收集过程中,都不会主动收集 Direct Buffer它的垃圾收集过程就是基于我在专栏前面所介绍的 Cleaner一个内部实现和幻象引用PhantomReference机制其本身不是 public 类型,内部实现了一个 Deallocator 负责销毁的逻辑。对它的销毁往往要拖到 full GC 的时候,所以使用不当很容易导致 OutOfMemoryError。</p>
@@ -1681,6 +1724,7 @@ throws IOException
<p>\5. 跟踪和诊断 Direct Buffer 内存占用?</p>
<p>因为通常的垃圾收集日志等记录,并不包含 Direct Buffer 等信息,所以 Direct Buffer 内存诊断也是个比较头疼的事情。幸好,在 JDK 8 之后的版本,我们可以方便地使用 Native Memory TrackingNMT特性来进行诊断你可以在程序启动时加上下面参数</p>
<pre><code>-XX:NativeMemoryTracking={summary|detail}
</code></pre>
<p>注意,激活 NMT 通常都会导致 JVM 出现 5%~10% 的性能下降,请谨慎考虑。</p>
<p>运行时,可以采用下面命令进行交互式对比:</p>
@@ -1774,12 +1818,14 @@ public short doSomething() {
<p><strong>OOP 原则实践中的取舍</strong></p>
<p>值得注意的是现代语言的发展很多时候并不是完全遵守前面的原则的比如Java 10 中引入了本地方法类型推断和 var 类型。按照,里氏替换原则,我们通常这样定义变量:</p>
<pre><code>List&lt;String&gt; list = new ArrayList&lt;&gt;();
</code></pre>
<p>如果使用 var 类型,可以简化为</p>
<pre><code>var list = new ArrayList&lt;String&gt;();
</code></pre>
<p>但是list 实际会被推断为“ArrayList &lt; String &gt;</p>
<pre><code>ArrayList&lt;String&gt; list = new ArrayList&lt;String&gt;();
</code></pre>
<p>理论上,这种语法上的便利,其实是增强了程序对实现的依赖,但是微小的类型泄漏却带来了书写的便利和代码可读性的提高,所以,实践中我们还是要按照得失利弊进行选择,而不是一味得遵循原则。</p>
<p><strong>OOP 原则在面试题目中的分析</strong></p>
@@ -1839,6 +1885,7 @@ public short doSomething() {
<p>因为装饰器模式本质上是包装同类型实例,我们对目标对象的调用,往往会通过包装类覆盖过的方法,迂回调用被包装的实例,这就可以很自然地实现增加额外逻辑的目的,也就是所谓的“装饰”。</p>
<p>例如BufferedInputStream 经过包装,为输入流过程增加缓存,类似这种装饰器还可以多次嵌套,不断地增加不同层次的功能。</p>
<pre><code>public BufferedInputStream(InputStream in)
</code></pre>
<p>我在下面的类图里,简单总结了 InputStream 的装饰模式实践。</p>
<p><img src="assets/77ad2dc2513da8155a3781e8291fac33.png" alt="img" /></p>
@@ -2038,10 +2085,12 @@ Observed data race, former is 13097, latter is 13099
<p>我会在下一讲,对 synchronized 和其他锁实现的更多底层细节进行深入分析。</p>
<p>代码中使用 synchronized 非常便利,如果用来修饰静态方法,其等同于利用下面代码将方法体囊括进来:</p>
<pre><code>synchronized (ClassName.class) {}
</code></pre>
<p>再来看看 ReentrantLock。你可能好奇什么是再入它是表示当一个线程试图获取一个它已经获取的锁时这个获取动作就自动成功这是对锁获取粒度的一个概念也就是锁的持有是以线程为单位而不是基于调用次数。Java 锁实现强调再入性是为了和 pthread 的行为进行区分。</p>
<p>再入锁可以设置公平性fairness我们可在创建再入锁时选择是否是公平的。</p>
<pre><code>ReentrantLock fairLock = new ReentrantLock(true);
</code></pre>
<p>这里所谓的公平性是指在竞争场景中,当公平性为真时,会倾向于将锁赋予等待时间最久的线程。公平性是减少线程“饥饿”(个别线程长期等待锁,但始终无法获取)情况发生的一个办法。</p>
<p>如果使用 synchronized我们根本<strong>无法进行</strong>公平性的选择其永远是不公平的这也是主流操作系统线程调度的选择。通用场景中公平性未必有想象中的那么重要Java 默认的调度策略很少会导致 “饥饿”发生。与此同时,若要保证公平性则会引入额外开销,自然会导致一定的吞吐量下降。所以,我建议<strong>只有</strong>当你的程序确实有公平性需要的时候,才有必要指定它。</p>
@@ -2305,6 +2354,7 @@ public ArrayBlockingQueue(int capacity, boolean fair) {
<li>计时等待TIMED_WAIT其进入条件和等待状态类似但是调用的是存在超时条件的方法比如 wait 或 join 等方法的指定超时版本,如下面示例:</li>
</ul>
<pre><code>public final native void wait(long timeout) throws InterruptedException;
</code></pre>
<ul>
<li>终止TERMINATED不管是意外退出还是正常执行结束线程已经完成使命终止运行也有人把这个状态叫作死亡。</li>
@@ -2467,6 +2517,7 @@ waitForAConfition(...);
<p>首先,可以使用 jps 或者系统的 ps 命令、任务管理器等工具,确定进程 ID。</p>
<p>其次,调用 jstack 获取线程栈:</p>
<pre><code>${JAVA_HOME}\bin\jstack your_pid
</code></pre>
<p>然后,分析得到的输出,具体片段如下:</p>
<p><img src="assets/1fcc1a521b801a5f7428d5229525a38b.png" alt="img" /></p>
@@ -2842,6 +2893,7 @@ void put(E e) throws InterruptedException;
<li>ArrayBlockingQueue 是最典型的的有界队列,其内部以 final 的数组保存数据,数组的大小就决定了队列的边界,所以我们在创建 ArrayBlockingQueue 时,都要指定容量,如</li>
</ul>
<pre><code>public ArrayBlockingQueue(int capacity, boolean fair)
</code></pre>
<ul>
<li>LinkedBlockingQueue容易被误解为无边界但其实其行为和内部代码都是基于有界的逻辑实现的只不过如果我们没有在创建队列时就指定容量那么其容量限制就自动被设置为 Integer.MAX_VALUE成为了无界队列。</li>
@@ -3133,6 +3185,7 @@ private volatile int value;
</code></pre>
<p>而类似 compareAndSet 这种返回 boolean 类型的函数,因为其返回值表现的就是成功与否,所以不需要重试。</p>
<pre><code>public final boolean compareAndSet(int expectedValue, int newValue)
</code></pre>
<p>CAS 是 Java 并发中所谓 lock-free 机制的基础。</p>
<h2>考点分析</h2>
@@ -3191,6 +3244,7 @@ private void acquireLock(){
<li>一个 volatile 的整数成员表征状态,同时提供了 setState 和 getState 方法</li>
</ul>
<pre><code>private volatile int state;
</code></pre>
<ul>
<li>一个先入先出FIFO的等待线程队列以实现多线程间竞争和等待这是 AQS 机制的核心之一。</li>
@@ -3361,6 +3415,7 @@ java -Xbootclasspath/p:&lt;your_dir&gt; your_App
</ul>
<p>首先,确认要修改的类文件已经编译好,并按照对应模块(假设是 java.base结构存放 然后,给模块打补丁:</p>
<pre><code>java --patch-module java.base=your_patch yourApp
</code></pre>
<ul>
<li>扩展类加载器被重命名为平台类加载器Platform Class-Loader而且 extension 机制则被移除。也就意味着,如果我们指定 java.ext.dirs 环境变量,或者 lib/ext 目录存在JVM 将直接返回<strong>错误</strong>!建议解决办法就是将其放入 classpath 里。</li>
@@ -3574,6 +3629,7 @@ cw.visitEnd(); // 结束类字节码生成
<li><a href="http://hg.openjdk.java.net/jdk/jdk/file/9f62267e79df/src/java.base/share/classes/java/nio/Bits.java">java.nio.BIts.reserveMemory()</a> 方法中我们能清楚的看到System.gc() 会被调用,以清理空间,这也是为什么在大量使用 NIO 的 Direct Buffer 之类时,通常建议不要加下面的参数,毕竟是个最后的尝试,有可能避免一定的内存不足问题。</li>
</ul>
<pre><code>-XX:+DisableExplictGC
</code></pre>
<p>当然也不是在任何情况下垃圾收集器都会被触发的比如我们去分配一个超大对象类似一个超大数组超过堆的最大值JVM 可以判断出垃圾收集并不能解决这个问题,所以直接抛出 OutOfMemoryError。</p>
<p>从我前面分析的数据区的角度,除了程序计数器,其他区域都有可能会因为可能的空间不足发生 OutOfMemoryError简单总结如下</p>
@@ -3637,27 +3693,32 @@ cw.visitEnd(); // 结束类字节码生成
<li>最大堆体积</li>
</ul>
<pre><code>-Xmx value
</code></pre>
<ul>
<li>初始的最小堆体积</li>
</ul>
<pre><code>-Xms value
</code></pre>
<ul>
<li>老年代和新生代的比例</li>
</ul>
<pre><code>-XX:NewRatio=value
</code></pre>
<p>默认情况下,这个数值是 2意味着老年代是新生代的 2 倍大;换句话说,新生代是堆大小的 1/3。</p>
<ul>
<li>当然,也可以不用比例的方式调整新生代的大小,直接指定下面的参数,设定具体的内存大小数值。</li>
</ul>
<pre><code>-XX:NewSize=value
</code></pre>
<ul>
<li>Eden 和 Survivor 的大小是按照比例设置的,如果 SurvivorRatio 是 8那么 Survivor 区域就是 Eden 的 1/8 大小,也就是新生代的 1/10因为 YoungGen=Eden + 2*SurvivorJVM 参数格式是</li>
</ul>
<pre><code>-XX:SurvivorRatio=value
</code></pre>
<ul>
<li>TLAB 当然也可以调整JVM 实现了复杂的适应策略,如果你有兴趣可以参考这篇<a href="https://blogs.oracle.com/jonthecollector/the-real-thing">说明</a></li>
@@ -3670,9 +3731,11 @@ cw.visitEnd(); // 结束类字节码生成
<p>接下来我会依赖 NMT 特性对 JVM 进行分析,它所提供的详细分类信息,非常有助于理解 JVM 内部实现。</p>
<p>首先来做些准备工作,开启 NMT 并选择 summary 模式,</p>
<pre><code>-XX:NativeMemoryTracking=summary
</code></pre>
<p>为了方便获取和对比 NMT 输出,选择在应用退出时打印 NMT 统计信息</p>
<pre><code>-XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics
</code></pre>
<p>然后,执行一个简单的在标准输出打印 HelloWorld 的程序,就可以得到下面的输出<img src="assets/55f1c7f0550adbbcc885c97a4dd426bb.png" alt="img" /></p>
<p>我来仔细分析一下NMT 所表征的 JVM 本地内存使用:</p>
@@ -3681,9 +3744,11 @@ cw.visitEnd(); // 结束类字节码生成
<li>第二部分是 Class 内存占用,它所统计的就是 Java 类元数据所占用的空间JVM 可以通过类似下面的参数调整其大小:</li>
</ul>
<pre><code>-XX:MaxMetaspaceSize=value
</code></pre>
<p>对于本例,因为 HelloWorld 没有什么用户类库所以其内存占用主要是启动类加载器Bootstrap加载的核心类库。你可以使用下面的小技巧调整启动类加载器元数据区这主要是为了对比以加深理解也许只有在 hack JDK 时才有实际意义。</p>
<pre><code>-XX:InitialBootClassLoaderMetaspaceSize=30720
</code></pre>
<ul>
<li>下面是 Thread这里既包括 Java 线程如程序主线程、Cleaner 线程等,也包括 GC 等本地线程。你有没有注意到,即使是一个 HelloWorld 程序,这个线程数量竟然还有 25。似乎有很多浪费设想我们要用 Java 作为 Serverless 运行时,每个 function 是非常短暂的,如何降低线程数量呢? 如果你充分理解了专栏讲解的内容,对 JVM 内部有了充分理解,思路就很清晰了: JDK 9 的默认 GC 是 G1虽然它在较大堆场景表现良好但本身就会比传统的 Parallel GC 或者 Serial GC 之类复杂太多,所以要么降低其并行线程数目,要么直接切换 GC 类型; JIT 编译默认是开启了 TieredCompilation 的,将其关闭,那么 JIT 也会变得简单,相应本地线程也会减少。 我们来对比一下,这是默认参数情况的输出:<img src="assets/97d060b306e44af3a8443f932a0a4d42.png" alt="img" /></li>
@@ -3694,7 +3759,9 @@ cw.visitEnd(); // 结束类字节码生成
<li>接下来是 Code 统计信息,显然这是 CodeCache 相关内存,也就是 JIT compiler 存储编译热点方法等信息的地方JVM 提供了一系列参数可以限制其初始值和最大值等,例如:</li>
</ul>
<pre><code>-XX:InitialCodeCacheSize=value
-XX:ReservedCodeCacheSize=value
</code></pre>
<p>你可以设置下列 JVM 参数,也可以只设置其中一个,进一步判断不同参数对 CodeCache 大小的影响。<img src="assets/945740c37433f783d2d877c67dcc1170.png" alt="img" /><img src="http://192.168.73.85:8080/%E6%9E%81%E5%AE%A2%E6%97%B6%E9%97%B4/Java%E5%9F%BA%E7%A1%8036%E8%AE%B2/assets/82d1fbc9ca09698c01ccff18fb97c8cd.png" alt="img" /></p>
<p>很明显CodeCache 空间下降非常大,这是因为我们关闭了复杂的 TieredCompilation而且还限制了其初始大小。</p>
@@ -3703,6 +3770,7 @@ cw.visitEnd(); // 结束类字节码生成
</ul>
<p>使用命令:</p>
<pre><code>-XX:+UseSerialGC
</code></pre>
<p><img src="assets/6eeee6624c7dc6be54bfce5e93064233.png" alt="img" /></p>
<p>可见不仅总线程数大大降低25 → 13而且 GC 设施本身的内存开销就少了非常多。据我所知AWS Lambda 中 Java 运行时就是使用的 Serial GC可以大大降低单个 function 的启动和运行开销。</p>
@@ -3723,17 +3791,20 @@ cw.visitEnd(); // 结束类字节码生成
<li>Serial GC它是最古老的垃圾收集器“Serial”体现在其收集工作是单线程的并且在进行垃圾收集过程中会进入臭名昭著的“Stop-The-World”状态。当然其单线程设计也意味着精简的 GC 实现,无需维护复杂的数据结构,初始化也简单,所以一直是 Client 模式下 JVM 的默认选项。 从年代的角度,通常将其老年代实现单独称作 Serial Old它采用了标记 - 整理Mark-Compact算法区别于新生代的复制算法。 Serial GC 的对应 JVM 参数是:</li>
</ul>
<pre><code>-XX:+UseSerialGC
</code></pre>
<ul>
<li>ParNew GC很明显是个新生代 GC 实现,它实际是 Serial GC 的多线程版本,最常见的应用场景是配合老年代的 CMS GC 工作,下面是对应参数</li>
</ul>
<pre><code>-XX:+UseConcMarkSweepGC -XX:+UseParNewGC
</code></pre>
<ul>
<li>CMSConcurrent Mark Sweep GC基于标记 - 清除Mark-Sweep算法设计目标是尽量减少停顿时间这一点对于 Web 等反应时间敏感的应用非常重要,一直到今天,仍然有很多系统使用 CMS GC。但是CMS 采用的标记 - 清除算法,存在着内存碎片化问题,所以难以避免在长时间运行等情况下发生 full GC导致恶劣的停顿。另外既然强调了并发ConcurrentCMS 会占用更多 CPU 资源,并和用户线程争抢。</li>
<li>Parrallel GC在早期 JDK 8 等版本中,它是 server 模式 JVM 的默认 GC 选择,也被称作是吞吐量优先的 GC。它的算法和 Serial GC 比较相似,尽管实现要复杂的多,其特点是新生代和老年代 GC 都是并行进行的,在常见的服务器环境中更加高效。 开启选项是:</li>
</ul>
<pre><code>-XX:+UseParallelGC
</code></pre>
<p>另外Parallel GC 引入了开发者友好的配置项我们可以直接设置暂停时间或吞吐量等目标JVM 会自动进行适应性调整,例如下面参数:</p>
<pre><code>-XX:MaxGCPauseMillis=value
@@ -3761,6 +3832,7 @@ cw.visitEnd(); // 结束类字节码生成
</ul>
<p>方法区无用元数据的回收比较复杂我简单梳理一下。还记得我对类加载器的分类吧一般来说初始化类加载器加载的类型是不会进行类卸载unload而普通的类型的卸载往往是要求相应自定义类加载器本身被回收所以大量使用动态类型的场合需要防止元数据区或者早期的永久代不会 OOM。在 8u40 以后的 JDK 中,下面参数已经是默认的:</p>
<pre><code>-XX:+ClassUnloadingWithConcurrentMark
</code></pre>
<p>第二,常见的垃圾收集算法,我认为总体上有个了解,理解相应的原理和优缺点,就已经足够了,其主要分为三类:</p>
<ul>
@@ -3777,6 +3849,7 @@ cw.visitEnd(); // 结束类字节码生成
<p>第二, 经过一次 Minor GCEden 就会空闲下来,直到再次达到 Minor GC 触发条件,这时候,另外一个 Survivor 区域则会成为 to 区域Eden 区域的存活对象和 From 区域对象,都会被复制到 to 区域,并且存活的年龄计数会被加 1。<img src="assets/3be4ac4834e2790a8211252f2bebfd48.png" alt="img" /></p>
<p>第三, 类似第二步的过程会发生很多次直到有对象年龄计数达到阈值这时候就会发生所谓的晋升Promotion过程如下图所示超过阈值的对象会被晋升到老年代。这个阈值是可以通过参数指定</p>
<pre><code>-XX:MaxTenuringThreshold=&lt;N&gt;
</code></pre>
<p><img src="assets/dbcb15c99b368773145b358734e10e8d.png" alt="img" /></p>
<p>后面就是老年代 GC具体取决于选择的 GC 选项,对应不同的算法。下面是一个简单标记 - 整理算法过程示意图,老年代中的无用对象被清除后, GC 会将对象进行整理,以防止内存碎片化。</p>
@@ -4096,16 +4169,20 @@ while (!initialized)
<p>首先,就是最常见的 SQL 注入攻击。一个典型的场景就是 Web 系统的用户登录功能,根据用户输入的用户名和密码,我们需要去后端数据库核实信息。</p>
<p>假设应用逻辑是,后端程序利用界面输入动态生成类似下面的 SQL然后让 JDBC 执行。</p>
<pre><code>Select * from use_info where username = “input_usr_name” and password = “input_pwd”
</code></pre>
<p>但是,如果我输入的 input_pwd 是类似下面的文本,</p>
<pre><code>“ or “”=”
</code></pre>
<p>那么,拼接出的 SQL 字符串就变成了下面的条件OR 的存在导致输入什么名字都是复合条件的。</p>
<pre><code>Select * from use_info where username = “input_usr_name” and password = “” or “” = “”
</code></pre>
<p>这里只是举个简单的例子,它是利用了期望输入和可能输入之间的偏差。上面例子中,期望用户输入一个数值,但实际输入的则是 SQL 语句片段。类似场景可以利用注入的不同 SQL 语句,进行各种不同目的的攻击,甚至还可以加上“;delete xxx”之类语句如果数据库权限控制不合理攻击效果就可能是灾难性的。</p>
<p>第二操作系统命令注入。Java 语言提供了类似 Runtime.exec(…) 的 API可以用来执行特定命令假设我们构建了一个应用以输入文本作为参数执行下面的命令</p>
<pre><code>ls la input_file_name
</code></pre>
<p>但是如果用户输入是 “input_file_name;rm rf /*”这就有可能出现问题了。当然这只是个举例Java 标准类库本身进行了非常多的改进,所以类似这种编程错误,未必可以真的完成攻击,但其反映的一类场景是真实存在的。</p>
<p>第三XML 注入攻击。Java 核心类库提供了全面的 XML 处理、转换等各种 API而 XML 自身是可以包含动态内容的,例如 XPATH如果使用不当可能导致访问恶意内容。</p>
@@ -4144,6 +4221,7 @@ while (!initialized)
</ul>
<p>在应用实践中,如果对安全要求非常高,建议打开 SecurityManager</p>
<pre><code>-Djava.security.manager
</code></pre>
<p>请注意其开销,通常只要开启 SecurityManager就会导致 10% ~ 15% 的性能下降,在 JDK 9 以后,这个开销有所改善。</p>
<p>理解了基础 Java 安全机制,接下来我们来一起探讨安全漏洞(<a href="https://en.wikipedia.org/wiki/Vulnerability_(computing)">Vulnerability</a>)。</p>
@@ -4195,6 +4273,7 @@ if (a + b &lt; c) {
<p>从语言特性来说Java 和 JVM 提供了很多基础性的改进,相比于传统的 C、C++ 等语言,对于数组越界等处理要完善的多,原生的避免了<a href="https://en.wikipedia.org/wiki/Buffer_overflow">缓冲区溢出</a>等攻击方式提高了软件的安全性。但这并不代表完全杜绝了问题Java 程序可能调用本地代码,也就是 JNI 技术,错误的数值可能导致 C/C++ 层面的数据越界等问题,这是很危险的。</p>
<p>所以,上面的条件判断,需要判断其数值范围,例如,写成类似下面结构。</p>
<pre><code>if (a &lt; c b)
</code></pre>
<p>再来看一个例子,请看下面的一段异常处理代码:</p>
<pre><code>try {
@@ -4283,17 +4362,20 @@ throw new RuntimeException(hostname + port + “ doesnt response”);
<li>利用 top 命令获取相应 pid“-H”代表 thread 模式,你可以配合 grep 命令更精准定位。</li>
</ul>
<pre><code>top H
</code></pre>
<ul>
<li>然后转换成为 16 进制。</li>
</ul>
<pre><code>printf &quot;%x&quot; your_pid
</code></pre>
<ul>
<li>最后利用 jstack 获取的线程栈,对比相应的 ID 即可。</li>
</ul>
<p>当然,还有更加通用的诊断方向,利用 vmstat 之类,查看上下文切换的数量,比如下面就是指定时间间隔为 1收集 10 次。</p>
<pre><code>vmstat -1 -10
</code></pre>
<p>输出如下:<img src="assets/abd28cb4a771365211e1a01d628213a0.png" alt="img" /></p>
<p>如果每秒上下文cs<a href="https://en.wikipedia.org/wiki/Context_switch">context switch</a>切换很高并且比系统中断高很多insystem <a href="https://en.wikipedia.org/wiki/Interrupt">interrupt</a>),就表明很有可能是因为不合理的多线程调度所导致。当然还需要利用<a href="https://linux.die.net/man/1/pidstat">pidstat</a>等手段,进行更加具体的定位,我就不再进一步展开了。</p>
@@ -4316,6 +4398,7 @@ throw new RuntimeException(hostname + port + “ doesnt response”);
<p>所以JFR/JMC 完全具备了生产系统 Profiling 的能力,目前也确实在真正大规模部署的云产品上使用过相关技术,快速地定位了问题。</p>
<p>它的使用也非常方便,你不需要重新启动系统或者提前增加配置。例如,你可以在运行时启动 JFR 记录,并将这段时间的信息写入文件:</p>
<pre><code>Jcmd &lt;pid&gt; JFR.start duration=120s filename=myrecording.jfr
</code></pre>
<p>然后,使用 JMC 打开“.jfr 文件”就可以进行分析了方法、异常、线程、IO 等应有尽有,其功能非常强大。如果你想了解更多细节,可以参考相关<a href="https://blog.takipi.com/oracle-java-mission-control-the-ultimate-guide/">指南</a></p>
<p>今天我从一个典型性能问题出发从症状表现到具体的系统分析、JVM 分析,系统性地整理了常见性能分析的思路;并且在知识扩展部分,从方法论和实际操作的角度,让你将理论和实际结合,相信一定可以对你有所帮助。</p>
@@ -4390,9 +4473,11 @@ public void testMethod() {
</code></pre>
<p>当我们实现了具体的测试后,就可以利用下面的 Maven 命令构建。</p>
<pre><code>mvn clean install
</code></pre>
<p>运行基准测试则与运行不同的 Java 应用没有明显区别。</p>
<pre><code>java -jar target/benchmarks.jar
</code></pre>
<p>更加具体的上手步骤,请参考相关<a href="http://www.baeldung.com/java-microbenchmark-harness">指南</a>。JMH 处处透着浓浓的工程师味道,并没有纠结于完善的文档,而是提供了非常棒的<a href="http://hg.openjdk.java.net/code-tools/jmh/file/3769055ad883/jmh-samples/src/main/java/org/openjdk/jmh/samples">样例代码</a>,所以你需要习惯于直接从代码中学习。</p>
<p><strong>如何保证微基准测试的正确性,有哪些坑需要规避?</strong></p>
@@ -4403,9 +4488,11 @@ public void testMethod() {
<li>保证代码经过了足够并且合适的预热。我在<a href="http://time.geekbang.org/column/article/6845">专栏第 1 讲</a>中提到过,默认情况,在 server 模式下JIT 会在一段代码执行 10000 次后将其编译为本地代码client 模式则是 1500 次以后。我们需要排除代码执行初期的噪音,保证真正采样到的统计数据符合其稳定运行状态。 通常建议使用下面的参数来判断预热工作到底是经过了多久。</li>
</ul>
<pre><code>-XX:+PrintCompilation
</code></pre>
<p>我这里建议考虑另外加上一个参数,否则 JVM 将默认开启后台编译,也就是在其他线程进行,可能导致输出的信息有些混淆。</p>
<pre><code>-Xbatch
</code></pre>
<p>与此同时,也要保证预热阶段的代码路径和采集阶段的代码路径是一致的,并且可以观察 PrintCompilation 输出是否在后期运行中仍然有零星的编译语句出现。</p>
<ul>
@@ -4444,6 +4531,7 @@ public void testMethod(MyState state, Blackhole blackhole) {
<li>如果你希望确定方法内联Inlining对性能的影响可以考虑打开下面的选项。</li>
</ul>
<pre><code>-XX:+PrintInlining
</code></pre>
<p>从上面的总结,可以看出来微基准测试是一个需要高度了解 Java、JVM 底层机制的技术,是个非常好的深入理解程序背后效果的工具,但是也反映了我们需要审慎对待微基准测试,不被可能的假象蒙蔽。</p>
<p>我今天介绍的内容是相对常见并易于把握的对于微基准测试GC 等基层机制同样会影响其统计数据。我在前面提到,微基准测试通常希望执行时间和内存分配速率都控制在有限范围内,而在这个过程中发生 GC很可能导致数据出现偏差所以 Serial GC 是个值得考虑的选项。另外JDK 11 引入了<a href="https://openjdk.java.net/jeps/318">Epsilon GC</a>,可以考虑使用这种什么也不做的 GC 方式,从最大可能性去排除相关影响。</p>
@@ -4487,14 +4575,17 @@ public void testMethod(MyState state, Blackhole blackhole) {
<li>打印编译发生的细节。</li>
</ul>
<pre><code>-XX:+PrintCompilation
</code></pre>
<ul>
<li>输出更多编译的细节。</li>
</ul>
<pre><code>-XX:UnlockDiagnosticVMOptions -XX:+LogCompilation -XX:LogFile=&lt;your_file_path&gt;
</code></pre>
<p>JVM 会生成一个 xml 形式的文件,另外, LogFile 选项是可选的,不指定则会输出到</p>
<pre><code>hotspot_pid&lt;pid&gt;.log
</code></pre>
<p>具体格式可以参考 Ben Evans 提供的<a href="https://github.com/AdoptOpenJDK/jitwatch/">JitWatch</a>工具和<a href="http://www.oracle.com/technetwork/articles/java/architect-evans-pt1-2266278.html">分析指南</a></p>
<p><img src="assets/07b00499b0ca857fc3ccd51f7046d946.png" alt="img" /></p>
@@ -4502,6 +4593,7 @@ public void testMethod(MyState state, Blackhole blackhole) {
<li>打印内联的发生,可利用下面的诊断选项,也需要明确解锁。</li>
</ul>
<pre><code>-XX:+PrintInlining
</code></pre>
<ul>
<li>如何知晓 Code Cache 的使用状态呢?</li>
@@ -4513,21 +4605,26 @@ public void testMethod(MyState state, Blackhole blackhole) {
</ul>
<p>我曾经介绍过 JIT 的默认门限server 模式默认 10000 次client 是 1500 次。门限大小也存在着调优的可能,可以使用下面的参数调整;与此同时,该参数还可以变相起到降低预热时间的作用。</p>
<pre><code>-XX:CompileThreshold=N
</code></pre>
<p>很多人可能会产生疑问,既然是热点,不是早晚会达到门限次数吗?这个还真未必,因为 JVM 会周期性的对计数的数值进行衰减操作,导致调用计数器永远不能达到门限值,除了可以利用 CompileThreshold 适当调整大小,还有一个办法就是关闭计数器衰减。</p>
<pre><code>-XX:-UseCounterDecay
</code></pre>
<p>如果你是利用 debug 版本的 JDK还可以利用下面的参数进行试验但是生产版本是不支持这个选项的。</p>
<pre><code>-XX:CounterHalfLifeTime
</code></pre>
<ul>
<li>调整 Code Cache 大小</li>
</ul>
<p>我们知道 JIT 编译的代码是存储在 Code Cache 中的,需要注意的是 Code Cache 是存在大小限制的,而且不会动态调整。这意味着,如果 Code Cache 太小,可能只有一小部分代码可以被 JIT 编译,其他的代码则没有选择,只能解释执行。所以,一个潜在的调优点就是调整其大小限制。</p>
<pre><code>-XX:ReservedCodeCacheSize=&lt;SIZE&gt;
</code></pre>
<p>当然,也可以调整其初始大小。</p>
<pre><code>-XX:InitialCodeCacheSize=&lt;SIZE&gt;
</code></pre>
<p>注意,在相对较新版本的 Java 中由于分层编译Tiered-Compilation的存在Code Cache 的空间需求大大增加,其本身默认大小也被提高了。</p>
<ul>
@@ -4535,6 +4632,7 @@ public void testMethod(MyState state, Blackhole blackhole) {
</ul>
<p>JVM 的编译器线程数目与我们选择的模式有关,选择 client 模式默认只有一个编译线程,而 server 模式则默认是两个,如果是当前最普遍的分层编译模式,则会根据 CPU 内核数目计算 C1 和 C2 的数值,你可以通过下面的参数指定的编译线程数。</p>
<pre><code>-XX:CICompilerCount=N
</code></pre>
<p>在强劲的多处理器环境中,增大编译线程数,可能更加充分的利用 CPU 资源,让预热等过程更加快速;但是,反之也可能导致编译线程争抢过多资源,尤其是当系统非常繁忙时。例如,系统部署了多个 Java 应用实例的时候,那么减小编译线程数目,则是可以考虑的。</p>
<p>生产实践中,也有人推荐在服务器上关闭分层编译,直接使用 server 编译器,虽然会导致稍慢的预热速度,但是可能在特定工作负载上会有微小的吞吐量提高。</p>
@@ -4543,6 +4641,7 @@ public void testMethod(MyState state, Blackhole blackhole) {
</ul>
<p>比如减少进入安全点。严格说它远远不只是发生在动态编译的时候GC 阶段发生的更加频繁,你可以利用下面选项诊断安全点的影响。</p>
<pre><code>-XX:+PrintSafepointStatistics XX:+PrintGCApplicationStoppedTime
</code></pre>
<p>注意,在 JDK 9 之后PrintGCApplicationStoppedTime 已经被移除了,你需要使用“-Xlog:safepoint”之类方式来指定。</p>
<p>很多优化阶段都可能和安全点相关,例如:</p>
@@ -4551,6 +4650,7 @@ public void testMethod(MyState state, Blackhole blackhole) {
<li>常规的锁优化阶段也可能发生,比如,偏斜锁的设计目的是为了避免无竞争时的同步开销,但是当真的发生竞争时,撤销偏斜锁会触发安全点,是很重的操作。所以,在并发场景中偏斜锁的价值其实是被质疑的,经常会明确建议关闭偏斜锁。</li>
</ul>
<pre><code>-XX:-UseBiasedLocking
</code></pre>
<p>主要的优化手段就介绍到这里,这些方法都是普通 Java 开发者就可以利用的。如果你想对 JVM 优化手段有更深入的了解,建议你订阅 JVM 专家郑雨迪博士的专栏。</p>
<h2>一课一练</h2>
@@ -4838,21 +4938,25 @@ public void testMethod(MyState state, Blackhole blackhole) {
<a href="/极客时间/Java错误示例100讲.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70998098df1d8b66","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709ba3a57eaefbdc","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
@@ -4866,12 +4970,14 @@ public void testMethod(MyState state, Blackhole blackhole) {
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
@@ -4881,5 +4987,7 @@ public void testMethod(MyState state, Blackhole blackhole) {
}
return "";
}
</script>
</html>

View File

@@ -12,7 +12,9 @@
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
@@ -25,61 +27,86 @@
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/极客时间/Java基础36讲.md.html">Java基础36讲.md.html</a>
</li>
<li>
<a class="current-tab" href="/极客时间/Java错误示例100讲.md.html">Java错误示例100讲.md.html</a>
</li>
<li>
<a href="/极客时间/Linux性能优化.md.html">Linux性能优化.md.html</a>
</li>
<li>
<a href="/极客时间/MySQL实战45讲.md.html">MySQL实战45讲.md.html</a>
</li>
<li>
<a href="/极客时间/从0开始学微服务.md.html">从0开始学微服务.md.html</a>
</li>
<li>
<a href="/极客时间/代码精进之路.md.html">代码精进之路.md.html</a>
</li>
<li>
<a href="/极客时间/持续交付36讲.md.html">持续交付36讲.md.html</a>
</li>
<li>
<a href="/极客时间/程序员进阶攻略.md.html">程序员进阶攻略.md.html</a>
</li>
<li>
<a href="/极客时间/趣谈网络协议.md.html">趣谈网络协议.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
@@ -95,6 +122,7 @@
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
@@ -107,7 +135,9 @@ function hide_canvas() {
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
@@ -139,21 +169,25 @@ function hide_canvas() {
<a href="/极客时间/Linux性能优化.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"7099809e5b658b66","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709ba3a9fabffbdc","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
@@ -167,12 +201,14 @@ function hide_canvas() {
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
@@ -182,5 +218,7 @@ function hide_canvas() {
}
return "";
}
</script>
</html>

View File

@@ -12,7 +12,9 @@
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
@@ -25,61 +27,86 @@
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/极客时间/Java基础36讲.md.html">Java基础36讲.md.html</a>
</li>
<li>
<a href="/极客时间/Java错误示例100讲.md.html">Java错误示例100讲.md.html</a>
</li>
<li>
<a class="current-tab" href="/极客时间/Linux性能优化.md.html">Linux性能优化.md.html</a>
</li>
<li>
<a href="/极客时间/MySQL实战45讲.md.html">MySQL实战45讲.md.html</a>
</li>
<li>
<a href="/极客时间/从0开始学微服务.md.html">从0开始学微服务.md.html</a>
</li>
<li>
<a href="/极客时间/代码精进之路.md.html">代码精进之路.md.html</a>
</li>
<li>
<a href="/极客时间/持续交付36讲.md.html">持续交付36讲.md.html</a>
</li>
<li>
<a href="/极客时间/程序员进阶攻略.md.html">程序员进阶攻略.md.html</a>
</li>
<li>
<a href="/极客时间/趣谈网络协议.md.html">趣谈网络协议.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
@@ -94,6 +121,8 @@
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
@@ -106,7 +135,9 @@ function hide_canvas() {
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
@@ -320,6 +351,7 @@ $ grep 'model name' /proc/cpuinfo | wc -l
<h3>场景一CPU 密集型进程</h3>
<p>首先,我们在第一个终端运行 stress 命令,模拟一个 CPU 使用率 100% 的场景:</p>
<pre><code>$ stress --cpu 1 --timeout 600
</code></pre>
<p>接着,在第二个终端运行 uptime 查看平均负载的变化情况:</p>
<pre><code># -d 参数表示高亮显示变化的区域
@@ -346,6 +378,7 @@ $ pidstat -u 5 1
<h3>场景二I/O 密集型进程</h3>
<p>首先还是运行 stress 命令,但这次模拟 I/O 压力,即不停地执行 sync</p>
<pre><code>$ stress -i 1 --timeout 600
</code></pre>
<p>还是在第二个终端运行 uptime 查看平均负载的变化情况:</p>
<pre><code>$ watch -d uptime
@@ -376,6 +409,7 @@ Linux 4.15.0 (ubuntu) 09/22/18 _x86_64_ (2 CPU)
<p>当系统中运行进程超出 CPU 运行能力时,就会出现等待 CPU 的进程。</p>
<p>比如,我们还是使用 stress但这次模拟的是 8 个进程:</p>
<pre><code>$ stress -c 8 --timeout 600
</code></pre>
<p>由于系统只有 2 个 CPU明显比 8 个进程要少得多,因而,系统的 CPU 处于严重过载状态,平均负载高达 7.97</p>
<pre><code>$ uptime
@@ -789,6 +823,7 @@ Time per request: 859.942 [ms] (mean)
<p>这次,我们在第二个终端,将测试的请求总数增加到 10000。这样当你在第一个终端使用性能分析工具时 Nginx 的压力还是继续。</p>
<p>继续在第二个终端,运行 ab 命令:</p>
<pre><code>$ ab -c 10 -n 10000 http://10.240.0.5:10000/
</code></pre>
<p>接着,回到第一个终端运行 top 命令,并按下数字 1 ,切换到每个 CPU 的使用率:</p>
<pre><code>$ top
@@ -910,6 +945,7 @@ Time per request: 1138.229 [ms] (mean)
<p>这次,我们在第二个终端,将测试的并发请求数改成 5同时把请求时长设置为 10 分钟(-t 600。这样当你在第一个终端使用性能分析工具时 Nginx 的压力还是继续的。</p>
<p>继续在第二个终端运行 ab 命令:</p>
<pre><code>$ ab -c 5 -t 600 http://192.168.0.10:10000/
</code></pre>
<p>然后,我们在第一个终端运行 top 命令,观察系统的 CPU 使用情况:</p>
<pre><code>$ top
@@ -1072,6 +1108,7 @@ $ perf report
<p>这时,你就得继续排查,为什么被调用的命令,会导致 CPU 使用率升高或 I/O 升高等问题。这些复杂场景的案例,我会在后面的综合实战里详细分析。</p>
<p>最后,在案例结束时,不要忘了清理环境,执行下面的 Docker 命令,停止案例中用到的 Nginx 进程:</p>
<pre><code>$ docker rm -f nginx phpfpm
</code></pre>
<h2>execsnoop</h2>
<p>在这个案例中,我们使用了 top、pidstat、pstree 等工具分析了系统 CPU 使用率高的问题,并发现 CPU 升高是短时进程 stress 导致的,但是整个分析过程还是比较复杂的。对于这类问题,有没有更好的方法监控呢?</p>
@@ -1159,6 +1196,7 @@ stress 30407 30405 0 /usr/local/bin/stress -t 1 -d 1
<h3>操作和分析</h3>
<p>安装完成后,我们首先执行下面的命令运行案例应用:</p>
<pre><code>$ docker run --privileged --name=app -itd feisky/app:iowait
</code></pre>
<p>然后,输入 ps 命令,确认案例应用已正常启动。如果一切正常,你应该可以看到如下所示的输出:</p>
<pre><code>$ ps aux | grep /app
@@ -1325,6 +1363,7 @@ $ perf report
<p>看来,罪魁祸首是 app 内部进行了磁盘的直接 I/O 啊!</p>
<p>下面的问题就容易解决了。我们接下来应该从代码层面分析,究竟是哪里出现了直接读请求。查看源码文件 <a href="https://github.com/feiskyer/linux-perf-examples/blob/master/high-iowait-process/app.c">app.c</a>,你会发现它果然使用了 O_DIRECT 选项打开磁盘,于是绕过了系统缓存,直接对磁盘进行读写。</p>
<pre><code>open(disk, O_RDONLY|O_DIRECT|O_LARGEFILE, 0755)
</code></pre>
<p>直接读写磁盘,对 I/O 敏感型应用(比如数据库系统)是很友好的,因为你可以在应用中,直接控制磁盘的读写。但在大部分情况下,我们最好还是通过系统缓存来优化磁盘 I/O换句话说删除 O_DIRECT 这个选项就是了。</p>
<p><a href="https://github.com/feiskyer/linux-perf-examples/blob/master/high-iowait-process/app-fix1.c">app-fix1.c</a> 就是修改后的文件,我也打包成了一个镜像文件,运行下面的命令,你就可以启动它了:</p>
@@ -1883,9 +1922,11 @@ $ stress-ng -i 1 --hdd 1 --timeout 600
<p>回忆一下我们学过的进程状态,你应该记得,等待 CPU 的进程已经在 CPU 的就绪队列中,处于运行状态;而等待 I/O 的进程则处于不可中断状态。</p>
<p>另外,不同版本的 sysbench 运行参数也不是完全一样的。比如,在案例 Ubuntu 18.04 中,运行 sysbench 的格式为:</p>
<pre><code>$ sysbench --threads=10 --max-time=300 threads run
</code></pre>
<p>而在 Ubuntu 16.04 中,运行格式则为(感谢 Haku 留言分享的执行命令):</p>
<pre><code>$ sysbench --num-threads=10 --max-time=300 --test=threads run
</code></pre>
<h2>问题 4无法模拟出 I/O 性能瓶颈,以及 I/O 压力过大的问题</h2>
<p><img src="assets/9e235aca4e92b68e84dba03881c591d8.png" alt="img" /></p>
@@ -1905,6 +1946,7 @@ $ stress-ng -i 1 --hdd 1 --timeout 600
</ul>
<p>你可以点击 <a href="https://github.com/feiskyer/linux-perf-examples/tree/master/high-iowait-process">Github</a> 查看它的源码,使用方法我写在了这里:</p>
<pre><code>$ docker run --privileged --name=app -itd feisky/app:iowait /app -d /dev/sdb -s 67108864 -c 20
</code></pre>
<p>案例运行后,你可以执行 docker logs 查看它的日志。正常情况下,你可以看到下面的输出:</p>
<pre><code>$ docker logs app
@@ -1934,6 +1976,7 @@ pling period of length delay. The process and memory reports are instantaneous
<p>这也是留言比较多的一个问题,在 CentOS 系统中,使用 perf 工具看不到函数名,只能看到一些 16 进制格式的函数地址。</p>
<p>其实,只要你观察一下 perf 界面最下面的那一行,就会发现一个警告信息:</p>
<pre><code>Failed to open /opt/bitnami/php/lib/php/extensions/opcache.so, continuing without symbols
</code></pre>
<p>这说明perf 找不到待分析进程依赖的库。当然实际上这个案例中有很多依赖库都找不到只不过perf 工具本身只在最后一行显示警告信息,所以你只能看到这一条警告。</p>
<p>这个问题,其实也是在分析 Docker 容器应用时,我们经常碰到的一个问题,因为容器应用依赖的库都在镜像里面。</p>
@@ -1972,9 +2015,11 @@ $ perf_4.9 report
<p>首先是 perf 工具的版本问题。在最后一步中,我们运行的工具是容器内部安装的版本 perf_4.9,而不是普通的 perf 命令。这是因为, perf 命令实际上是一个软连接,会跟内核的版本进行匹配,但镜像里安装的 perf 版本跟虚拟机的内核版本有可能并不一致。</p>
<p>另外php-fpm 镜像是基于 Debian 系统的,所以安装 perf 工具的命令,跟 Ubuntu 也并不完全一样。比如, Ubuntu 上的安装方法是下面这样:</p>
<pre><code>$ apt-get install -y linux-tools-common linux-tools-generic linux-tools-$(uname -r)
</code></pre>
<p>而在 php-fpm 容器里,你应该执行下面的命令来安装 perf</p>
<pre><code>$ apt-get install -y linux-perf
</code></pre>
<p>当你按照前面这几种方法操作后,你就可以在容器内部看到 sqrt 的堆栈:</p>
<p><img src="assets/76f8d0f36210001e750b0a82026dedaf.png" alt="img" /></p>
@@ -2034,6 +2079,7 @@ $ perf_4.9 report
<p>threshold 的默认值为 0.5%,也就是说,事件比例超过 0.5% 时,调用栈才能被显示。再观察我们案例应用 app 的事件比例,只有 0.34%,低于 0.5%,所以看不到 app 的调用栈就很正常了。</p>
<p>这种情况下,你只需要给 perf report 设置一个小于 0.34% 的阈值,就可以显示我们想看到的调用图了。比如执行下面的命令:</p>
<pre><code>$ perf report -g graph,0.3
</code></pre>
<p>你就可以得到下面这个新的输出界面,展开 app 后,就可以看到它的调用栈了。</p>
<p><img src="assets/b34f95617d13088671f4d9c2b9134693.png" alt="img" /></p>
@@ -2144,6 +2190,7 @@ $ perf_4.9 report
<p>oom_adj 的范围是 [-17, 15],数值越大,表示进程越容易被 OOM 杀死;数值越小,表示进程越不容易被 OOM 杀死,其中 -17 表示禁止 OOM。</p>
<p>比如用下面的命令,你就可以把 sshd 进程的 oom_adj 调小为 -16这样 sshd 进程就不容易被 OOM 杀死。</p>
<pre><code>echo -16 &gt; /proc/$(pidof sshd)/oom_adj
</code></pre>
<h2>如何查看内存使用情况</h2>
<p>通过了解内存空间的分布,以及内存的分配和回收,我想你对内存的工作原理应该有了大概的认识。当然,系统的实际工作原理更加复杂,也会涉及其他一些机制,这里我只讲了最主要的原理。掌握了这些,你可以对内存的运作有一条主线认识,不至于脑海里只有术语名词的堆砌。</p>
@@ -2292,6 +2339,7 @@ r b swpd free buff cache si so bi bo in cs us sy id wa st
<p>正常情况下,空闲系统中,你应该看到的是,这几个值在多次结果中一直保持不变。</p>
<p>接下来,到第二个终端执行 dd 命令,通过读取随机设备,生成一个 500MB 大小的文件:</p>
<pre><code>$ dd if=/dev/urandom of=/tmp/file bs=1M count=500
</code></pre>
<p>然后再回到第一个终端,观察 Buffer 和 Cache 的变化情况:</p>
<pre><code>procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
@@ -2426,6 +2474,7 @@ sudo apt-get install -y bcc-tools libbcc-examples linux-headers-$(uname -r)
</blockquote>
<p>操作完这些步骤bcc 提供的所有工具就都安装到 /usr/share/bcc/tools 这个目录中了。不过这里提醒你bcc 软件包默认不会把这些工具配置到系统的 PATH 路径中,所以你得自己手动配置:</p>
<pre><code>$ export PATH=$PATH:/usr/share/bcc/tools
</code></pre>
<p>配置完,你就可以运行 cachestat 和 cachetop 命令了。比如,下面就是一个 cachestat 的运行界面,它以 1 秒的时间间隔,输出了 3 组缓存统计数据:</p>
<pre><code>$ cachestat 1 3
@@ -2559,6 +2608,7 @@ $ cachetop 5
</code></pre>
<p>接着,再到第二个终端,执行下面的命令运行案例:</p>
<pre><code>$ docker run --privileged --name=app -itd feisky/app:io-direct
</code></pre>
<p>案例运行后,我们还需要运行下面这个命令,来确认案例已经正常启动。如果一切正常,你应该可以看到类似下面的输出:</p>
<pre><code>$ docker logs app
@@ -2584,7 +2634,7 @@ strace: Process 4988 attached
restart_syscall(&lt;\.\.\. resuming interrupted nanosleep \.\.\.&gt;) = 0
openat(AT_FDCWD, &quot;/dev/sdb1&quot;, O_RDONLY|O_DIRECT) = 4
mmap(NULL, 33558528, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f448d240000
read(4, &quot;8vq\213\314\264u\373\4\336K\224\<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="53616613">[email&#160;protected]</a>\371\1\252\2\262\252q\221\n0\30\225bD\252\<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="85b7b3b3c5cf">[email&#160;protected]</a>&quot;\.\.\., 33554432) = 33554432
read(4, &quot;8vq\213\314\264u\373\4\336K\224\<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="1b292e5b">[email&#160;protected]</a>\371\1\252\2\262\252q\221\n0\30\225bD\252\<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="a3919595e3e9">[email&#160;protected]</a>&quot;\.\.\., 33554432) = 33554432
write(1, &quot;Time used: 0.948897 s to read 33&quot;\.\.\., 45) = 45
close(4) = 0
</code></pre>
@@ -2690,6 +2740,7 @@ sudo apt-get install -y bcc-tools libbcc-examples linux-headers-$(uname -r)
<p>如果安装过程中有什么问题,同样鼓励你先自己搜索解决,解决不了的,可以在留言区向我提问。如果你以前已经安装过了,就可以忽略这一点了。</p>
<p>安装完成后,再执行下面的命令来运行案例:</p>
<pre><code>$ docker run --name=app -itd feisky/app:mem-leak
</code></pre>
<p>案例成功运行后,你需要输入下面的命令,确认案例应用已经正常启动。如果一切正常,你应该可以看到下面这个界面:</p>
<pre><code>$ docker logs app
@@ -3089,9 +3140,11 @@ polkitd 1004 44 kB
<p>这也说明了一点,虽然缓存属于可回收内存,但在类似大文件拷贝这类场景下,系统还是会用 Swap 机制来回收匿名内存,而不仅仅是回收占用绝大部分内存的文件页。</p>
<p>最后,如果你在一开始配置了 Swap不要忘记在案例结束后关闭。你可以运行下面的命令关闭 Swap</p>
<pre><code>$ swapoff -a
</code></pre>
<p>实际上,关闭 Swap 后再重新打开,也是一种常用的 Swap 空间清理方法,比如:</p>
<pre><code>$ swapoff -a &amp;&amp; swapon -a
</code></pre>
<h2>小结</h2>
<p>在内存资源紧张时Linux 会通过 Swap ,把不常访问的匿名页换出到磁盘中,下次访问的时候再从磁盘换入到内存中来。你可以设置 /proc/sys/vm/min_free_kbytes来调整系统定期回收内存的阈值也可以设置 /proc/sys/vm/swappiness来调整文件页和匿名页的回收倾向。</p>
@@ -3494,6 +3547,7 @@ Minimum / Average / Maximum Object : 0.01K / 0.20K / 22.88K
<h2>思考</h2>
<p>最后,给你留一个思考题。在实际工作中,我们经常会根据文件名字,查找它所在路径,比如:</p>
<pre><code>$ find / -name file-name
</code></pre>
<p>今天的问题就是,这个命令,会不会导致系统的缓存升高呢?如果有影响,又会导致哪种类型的缓存升高呢?你可以结合今天内容,自己先去操作和分析,看看观察到的结果跟你分析的是否一样。</p>
<p>欢迎在留言区和我讨论,也欢迎把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。</p>
@@ -3681,6 +3735,7 @@ Actual DISK READ: 0.00 B/s | Actual DISK WRITE: 0.00 B/s
<h2>案例分析</h2>
<p>首先,我们在终端中执行下面的命令,运行今天的目标应用:</p>
<pre><code>$ docker run -v /tmp:/tmp --name=app -itd feisky/logapp
</code></pre>
<p>然后,在终端中运行 ps 命令,确认案例应用正常启动。如果操作无误,你应该可以在 ps 的输出中,看到一个 app.py 的进程:</p>
<pre><code>$ ps -ef | grep /app.py
@@ -3799,6 +3854,7 @@ signal.signal(signal.SIGUSR2, set_logging_warning)
<p>根据源码中的日志调用 logger. info(message) ,我们知道,它的日志是 INFO 级,这也正是它的默认级别。那么,只要把默认级别调高到 WARNING 级,日志问题应该就解决了。</p>
<p>接下来,我们就来检查一下,刚刚的分析对不对。在终端中运行下面的 kill 命令,给进程 18940 发送 SIGUSR2 信号:</p>
<pre><code>$ kill -SIGUSR2 18940
</code></pre>
<p>然后,再执行 top 和 iostat 观察一下:</p>
<pre><code>$ top
@@ -3814,6 +3870,7 @@ sda 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.0
<p>到这里,我们不仅定位了狂打日志的应用程序,并通过调高日志级别的方法,完美解决了 I/O 的性能瓶颈。</p>
<p>案例最后,当然不要忘了运行下面的命令,停止案例应用:</p>
<pre><code>$ docker rm -f app
</code></pre>
<h2>小结</h2>
<p>日志,是了解应用程序内部运行情况,最常用、也最有效的工具。无论是操作系统,还是应用程序,都会记录大量的运行日志,以便事后查看历史记录。这些日志一般按照不同级别来开启,比如,开发环境通常打开调试级别的日志,而线上环境则只记录警告和错误日志。</p>
@@ -3852,6 +3909,7 @@ sda 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.0
<h2><strong>案例分析</strong></h2>
<p>首先,我们在第一个终端中执行下面的命令,运行本次案例要分析的目标应用:</p>
<pre><code>$ docker run --name=app -p 10000:80 -itd feisky/word-pop
</code></pre>
<p>然后,在第二个终端中运行 curl 命令,访问 <a href="http://192.168.0.10:1000/">http://192.168.0.10:1000/</a>,确认案例正常启动。你应该可以在 curl 的输出界面里,看到一个 hello world 的输出:</p>
<pre><code>$ curl http://192.168.0.10:10000/
@@ -3859,6 +3917,7 @@ hello world
</code></pre>
<p>接下来,在第二个终端中,访问案例应用的单词热度接口,也就是 <a href="http://192.168.0.10:1000/popularity/word">http://192.168.0.10:1000/popularity/word</a></p>
<pre><code>$ curl http://192.168.0.10:1000/popularity/word
</code></pre>
<p>稍等一会儿,你会发现,这个接口居然这么长时间都没响应,究竟是怎么回事呢?我们先回到终端一来分析一下。</p>
<p>我们试试在第一个终端里,随便执行一个命令,比如执行 df 命令,查看一下文件系统的使用情况。奇怪的是,这么简单的命令,居然也要等好久才有输出。</p>
@@ -3872,6 +3931,7 @@ tmpfs 816932 1188 815744 1% /run
<p>这里的思路其实跟上一个案例比较类似,我们可以先用 top 来观察 CPU 和内存的使用情况,然后再用 iostat 来观察磁盘的 I/O 情况。</p>
<p>为了避免分析过程中 curl 请求突然结束,我们回到终端二,按 Ctrl+C 停止刚才的应用程序;然后,把 curl 命令放到一个循环里执行;这次我们还要加一个 time 命令,观察每次的执行时间:</p>
<pre><code>$ while true; do time curl http://192.168.0.10:10000/popularity/word; sleep 1; done
</code></pre>
<p>继续回到终端一来分析性能。我们在终端一中运行 top 命令,观察 CPU 和内存的使用情况:</p>
<pre><code>$ top
@@ -3925,6 +3985,7 @@ stat(&quot;/usr/local/lib/python3.7/importlib/_bootstrap.py&quot;, {st_mode=S_IF
<p>从 strace 中,你可以看到大量的 stat 系统调用,并且大都为 python 的文件,但是,请注意,这里并没有任何 write 系统调用。</p>
<p>由于 strace 的输出比较多,我们可以用 grep ,来过滤一下 write比如</p>
<pre><code>$ strace -p 12280 2&gt;&amp;1 | grep write
</code></pre>
<p>遗憾的是,这里仍然没有任何输出。</p>
<p>难道此时已经没有性能问题了吗?重新执行刚才的 top 和 iostat 命令,你会不幸地发现,性能问题仍然存在。</p>
@@ -4128,6 +4189,7 @@ Got data: () in 15.364538192749023 sec
<p>不过别急,在具体分析前,为了避免在分析过程中客户端的请求结束,我们把 curl 命令放到一个循环里执行。同时,为了避免给系统过大压力,我们设置在每次查询后,都先等待 5 秒,然后再开始新的请求。</p>
<p>所以,你可以在终端二中,继续执行下面的命令:</p>
<pre><code>$ while true; do curl http://192.168.0.10:10000/products/geektime; sleep 5; done
</code></pre>
<p>接下来,重新回到终端一中,分析接口响应速度慢的原因。不过,重回终端一后,你会发现系统响应也明显变慢了,随便执行一个命令,都得停顿一会儿才能看到输出。</p>
<p>这跟上一节的现象很类似,看来,我们还是得观察一下系统的资源使用情况,比如 CPU、内存和磁盘 I/O 等的情况。</p>
@@ -4176,6 +4238,7 @@ $ pidstat -d 1
</code></pre>
<p>观察一会,你会发现,线程 28014 正在读取大量数据,且读取文件的描述符编号为 38。这儿的 38 又对应着哪个文件呢?我们可以执行下面的 lsof 命令,并且指定线程号 28014 ,具体查看这个可疑线程和可疑文件:</p>
<pre><code>$ lsof -p 28014
</code></pre>
<p>奇怪的是lsof 并没有给出任何输出。实际上,如果你查看 lsof 命令的返回值,就会发现,这个命令的执行失败了。</p>
<p>我们知道,在 SHELL 中,特殊标量 $? 表示上一条命令退出时的返回值。查看这个特殊标量,你会发现它的返回值是 1。可是别忘了在 Linux 中,返回值为 0 ,才表示命令执行成功。返回值为 1显然表明执行失败。</p>
@@ -4234,7 +4297,9 @@ db.opt products.MYD products.MYI products.frm
<p>既然已经找出了数据库和表,接下来要做的,就是弄清楚数据库中正在执行什么样的 SQL 了。我们继续在终端一中,运行下面的 docker exec 命令,进入 MySQL 的命令行界面:</p>
<pre><code>$ docker exec -i -t mysql mysql
...
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql&gt;
</code></pre>
<p>下一步你应该可以想到,那就是在 MySQL 命令行界面中,执行 show processlist 命令,来查看当前正在执行的 SQL 语句。</p>
@@ -4320,6 +4385,7 @@ Got data: ()in 0.003951072692871094 sec
<p>首先,我们在终端二中停止 curl 命令,然后回到终端一中,执行下面的命令删除索引:</p>
<pre><code># 删除索引
$ docker exec -i -t mysql mysql
mysql&gt; use test;
mysql&gt; DROP INDEX products_index ON products;
</code></pre>
@@ -4426,6 +4492,7 @@ $ curl http://192.168.0.10:10000/init/5000
<p>到底出了什么问题呢?我们还是要用前面学过的性能工具和原理,来找到这个瓶颈。</p>
<p>不过别急,同样为了避免分析过程中客户端的请求结束,在进行性能分析前,我们先要把 curl 命令放到一个循环里来执行。你可以在终端二中,继续执行下面的命令:</p>
<pre><code>$ while true; do curl http://192.168.0.10:10000/get_cache; done
</code></pre>
<p>接下来,再重新回到终端一,查找接口响应慢的“病因”。</p>
<p>最近几个案例的现象都是响应很慢,这种情况下,我们自然先会怀疑,是不是系统资源出现了瓶颈。所以,先观察 CPU、内存和磁盘 I/O 等的使用情况肯定不会错。</p>
@@ -4571,6 +4638,7 @@ OK
</code></pre>
<p>改完后,切换到终端二中查看,你会发现,现在的请求时间,已经缩短到了 0.9s</p>
<pre><code>{..., &quot;elapsed_seconds&quot;:0.9368953704833984,&quot;type&quot;:&quot;good&quot;}
</code></pre>
<p>而第二个问题,就要查看应用的源码了。点击 <a href="https://github.com/feiskyer/linux-perf-examples/blob/master/redis-slow/app.py">Github</a> ,你就可以查看案例应用的源代码:</p>
<pre><code>def get_cache(type_name):
@@ -4592,6 +4660,7 @@ OK
<p>你可以发现,解决第二个问题后,新接口的性能又有了进一步的提升,从刚才的 0.9s ,再次缩短成了不到 0.2s。</p>
<p>当然,案例最后,不要忘记清理案例应用。你可以切换到终端一中,执行下面的命令进行清理:</p>
<pre><code>$ docker rm -f app redis
</code></pre>
<h2>小结</h2>
<p>今天我带你一起分析了一个 Redis 缓存的案例。</p>
@@ -5038,6 +5107,7 @@ eth0: flags=4163&lt;UP,BROADCAST,RUNNING,MULTICAST&gt; mtu 1500
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 32637401 bytes 4815573306 (4.8 GB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
$ ip -s addr show dev eth0
2: eth0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc mq state UP group default qlen 1000
link/ether 78:0d:3a:07:cf:3a brd ff:ff:ff:ff:ff:ff
@@ -5419,6 +5489,7 @@ $ yum install -y httpd-tools
</code></pre>
<p>接下来,在目标机器上,使用 Docker 启动一个 Nginx 服务,然后用 ab 来测试它的性能。首先,在目标机器上运行下面的命令:</p>
<pre><code>$ docker run -p 80:80 -itd nginx
</code></pre>
<p>而在另一台机器上,运行 ab 命令,测试 Nginx 的性能:</p>
<pre><code># -c 表示并发请求数为 1000-n 表示总的请求数为 10000
@@ -5507,6 +5578,7 @@ end
</code></pre>
<p>而在执行测试时,通过 -s 选项,执行脚本的路径:</p>
<pre><code>$ wrk -c 1000 -t 2 -s auth.lua http://192.168.0.30/
</code></pre>
<p>wrk 需要你用 Lua 脚本,来构造请求负载。这对于大部分场景来说,可能已经足够了 。不过,它的缺点也正是,所有东西都需要代码来构造,并且工具本身不提供 GUI 环境。</p>
<p>像 Jmeter 或者 LoadRunner商业产品则针对复杂场景提供了脚本录制、回放、GUI 等更丰富的功能,使用起来也更加方便。</p>
@@ -5650,7 +5722,7 @@ nameserver 114.114.114.114
<p>首先,执行下面的命令,进入今天的第一个案例。如果一切正常,你将可以看到下面这个输出:</p>
<pre><code># 进入案例环境的 SHELL 终端中
$ docker run -it --rm -v $(mktemp):/etc/resolv.conf feisky/dnsutils bash
<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="98eaf7f7ecd8affda1fdfcaefdfcaca1afac">[email&#160;protected]</a>:/#
<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="84f6ebebf0c4b3e1bde1e0b2e1e0b0bdb3b0">[email&#160;protected]</a>:/#
</code></pre>
<p>注意,这儿 root 后面的 7e9ed6ed4974是 Docker 生成容器的 ID 前缀,你的环境中很可能是不同的 ID所以直接忽略这一项就可以了。</p>
<blockquote>
@@ -5680,6 +5752,7 @@ round-trip min/avg/max/stddev = 31.116/31.163/31.245/0.058 ms
<p>从这次的输出可以看到nslookup 连接环回地址127.0.0.1 和 ::1的 53 端口失败。这里就有问题了,为什么会去连接环回地址,而不是我们的先前看到的 114.114.114.114 呢?</p>
<p>你可能已经想到了症结所在——有可能是因为容器中没有配置 DNS 服务器。那我们就执行下面的命令确认一下:</p>
<pre><code>/# cat /etc/resolv.conf
</code></pre>
<p>果然,这个命令没有任何输出,说明容器里的确没有配置 DNS 服务器。到这一步,很自然的,我们就知道了解决方法。在 /etc/resolv.conf 文件中,配置上 DNS 服务器就可以了。</p>
<p>你可以执行下面的命令,在配置好 DNS 服务器后,重新执行 nslookup 命令。自然,我们现在发现,这次可以正常解析了:</p>
@@ -5695,7 +5768,7 @@ Address: 39.106.233.176
<h3>案例 2DNS 解析不稳定</h3>
<p>接下来,我们再来看第二个案例。执行下面的命令,启动一个新的容器,并进入它的终端中:</p>
<pre><code>$ docker run -it --rm --cap-add=NET_ADMIN --dns 8.8.8.8 feisky/dnsutils bash
<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="dba9b4b4af9bebb8bfe8bebeebb8e3beb8b9">[email&#160;protected]</a>:/#
<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="fa8895958ebaca999ec99f9fca99c29f9998">[email&#160;protected]</a>:/#
</code></pre>
<p>然后,跟上一个案例一样,还是运行 nslookup 命令,解析 time.geekbang.org 的 IP 地址。不过,这次要加一个 time 命令,输出解析所用时间。如果一切正常,你可能会看到如下输出:</p>
<pre><code>/# time nslookup time.geekbang.org
@@ -5894,6 +5967,7 @@ sys 0m0.003s
<p>到这里,再往后该怎么分析呢?其实,这时候就可以用 tcpdump 抓包,查看 ping 在收发哪些网络包。</p>
<p>我们再打开另一个终端终端二SSH 登录案例机器后,执行下面的命令:</p>
<pre><code>$ tcpdump -nn udp port 53 or host 35.190.27.188
</code></pre>
<p>当然,你可以直接用 tcpdump 不加任何参数来抓包,但那样的话,就可能抓取到很多不相干的包。由于我们已经执行过 ping 命令,知道了 geekbang.org 的 IP 地址是 35.190.27.188,也知道 ping 命令会执行 DNS 查询。所以,上面这条命令,就是基于这个规则进行过滤。</p>
<p>我来具体解释一下这条命令。</p>
@@ -5954,6 +6028,7 @@ rtt min/avg/max/mdev = 32.879/35.160/39.030/2.755 ms
<p>到这里, 我就带你一起使用 tcpdump ,解决了一个最常见的 ping 工作缓慢的问题。</p>
<p>案例最后,如果你在开始时,执行了 iptables 命令,那也不要忘了删掉它:</p>
<pre><code>$ iptables -D INPUT -p udp --sport 53 -m string --string googleusercontent --algo bm -j DROP
</code></pre>
<p>不过,删除后你肯定还有疑问,明明我们的案例跟 Google 没啥关系,为什么要根据 googleusercontent ,这个毫不相关的字符串来过滤包呢?</p>
<p>实际上,如果换一个 DNS 服务器,就可以用 PTR 反查到 35.190.27.188 所对应的域名:</p>
@@ -5983,6 +6058,7 @@ Authoritative answers can be found from:
<p><img src="assets/4870a28c032bdd2a26561604ae2f7cb3.png" alt="img" /></p>
<p>最后,再次强调 tcpdump 的输出格式,我在前面已经介绍了它的基本格式:</p>
<pre><code>时间戳 协议 源地址. 源端口 &gt; 目的地址. 目的端口 网络包详细信息
</code></pre>
<p>其中,网络包的详细信息取决于协议,不同协议展示的格式也不同。所以,更详细的使用方法,还是需要你去查询 tcpdump 的 <a href="https://www.tcpdump.org/manpages/tcpdump.1.html">man</a> 手册(执行 man tcpdump 也可以得到)。</p>
<p>不过讲了这么多你应该也发现了。tcpdump 虽然功能强大,可是输出格式却并不直观。特别是,当系统中网络包数比较多(比如 PPS 超过几千)的时候,你想从 tcpdump 抓取的网络包中分析问题,实在不容易。</p>
@@ -5991,9 +6067,11 @@ Authoritative answers can be found from:
<p>Wireshark 也是最流行的一个网络分析工具,它最大的好处就是提供了跨平台的图形界面。跟 tcpdump 类似Wireshark 也提供了强大的过滤规则表达式,同时,还内置了一系列的汇总分析工具。</p>
<p>比如,拿刚刚的 ping 案例来说,你可以执行下面的命令,把抓取的网络包保存到 ping.pcap 文件中:</p>
<pre><code>$ tcpdump -nn udp port 53 or host 35.190.27.188 -w ping.pcap
</code></pre>
<p>接着,把它拷贝到你安装有 Wireshark 的机器中,比如你可以用 scp 把它拷贝到本地来:</p>
<pre><code>$ scp host-ip/path/ping.pcap .
</code></pre>
<p>然后,再用 Wireshark 打开它。打开后,你就可以看到下面这个界面:</p>
<p><img src="assets/6b854703dcfcccf64c0a69adecf2f42c.png" alt="img" /></p>
@@ -6013,6 +6091,7 @@ $ tcpdump -nn host 93.184.216.34 -w web.pcap
</blockquote>
<p>接下来,切换到终端二,执行下面的 curl 命令,访问 <a href="http://example.com/">http://example.com</a></p>
<pre><code>$ curl http://example.com
</code></pre>
<p>最后,再回到终端一,按下 Ctrl+C 停止 tcpdump并把得到的 web.pcap 拷贝出来。</p>
<p>使用 Wireshark 打开 web.pcap 后,你就可以在 Wireshark 中,看到如下的界面:</p>
@@ -6085,21 +6164,25 @@ $ tcpdump -nn host 93.184.216.34 -w web.pcap
<a href="/极客时间/MySQL实战45讲.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script><script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709980a0a8ba8b66","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script><script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709ba3ab1c04fbdc","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
@@ -6113,12 +6196,14 @@ $ tcpdump -nn host 93.184.216.34 -w web.pcap
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
@@ -6128,5 +6213,7 @@ $ tcpdump -nn host 93.184.216.34 -w web.pcap
}
return "";
}
</script>
</html>

View File

@@ -12,7 +12,9 @@
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
@@ -25,61 +27,86 @@
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/极客时间/Java基础36讲.md.html">Java基础36讲.md.html</a>
</li>
<li>
<a href="/极客时间/Java错误示例100讲.md.html">Java错误示例100讲.md.html</a>
</li>
<li>
<a href="/极客时间/Linux性能优化.md.html">Linux性能优化.md.html</a>
</li>
<li>
<a class="current-tab" href="/极客时间/MySQL实战45讲.md.html">MySQL实战45讲.md.html</a>
</li>
<li>
<a href="/极客时间/从0开始学微服务.md.html">从0开始学微服务.md.html</a>
</li>
<li>
<a href="/极客时间/代码精进之路.md.html">代码精进之路.md.html</a>
</li>
<li>
<a href="/极客时间/持续交付36讲.md.html">持续交付36讲.md.html</a>
</li>
<li>
<a href="/极客时间/程序员进阶攻略.md.html">程序员进阶攻略.md.html</a>
</li>
<li>
<a href="/极客时间/趣谈网络协议.md.html">趣谈网络协议.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
@@ -94,6 +121,8 @@
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
@@ -106,7 +135,9 @@ function hide_canvas() {
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
@@ -1287,19 +1318,19 @@ mysql&gt; alter table SUser add index index2(email(6));
<p>从图中你可以看到,由于 email(6) 这个索引结构中每个邮箱字段都只取前 6 个字节zhangs所以占用的空间会更小这就是使用前缀索引的优势。</p>
<p>但,这同时带来的损失是,可能会增加额外的记录扫描次数。</p>
<p>接下来,我们再看看下面这个语句,在这两个索引定义下分别是怎么执行的。</p>
<pre><code>select id,name,email from SUser where email='<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="dfa5b7beb1b8acaca7a6a59fa7a7a7f1bcb0b2">[email&#160;protected]</a>';
<pre><code>select id,name,email from SUser where email='<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="b8c2d0d9d6dfcbcbc0c1c2f8c0c0c096dbd7d5">[email&#160;protected]</a>';
</code></pre>
<p><strong>如果使用的是 index1</strong>(即 email 整个字符串的索引结构),执行顺序是这样的:</p>
<ol>
<li>从 index1 索引树找到满足索引值是’<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="8df7e5ece3eafefef5f4f7cdf5f5f5a3eee2e0">[email&#160;protected]</a>’的这条记录,取得 ID2 的值;</li>
<li>从 index1 索引树找到满足索引值是’<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="473d2f26292034343f3e3d073f3f3f6924282a">[email&#160;protected]</a>’的这条记录,取得 ID2 的值;</li>
<li>到主键上查到主键值是 ID2 的行,判断 email 的值是正确的,将这行记录加入结果集;</li>
<li>取 index1 索引树上刚刚查到的位置的下一条记录,发现已经不满足 email='<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="7c06141d121b0f0f0405063c040404521f1311">[email&#160;protected]</a>’的条件了,循环结束。</li>
<li>取 index1 索引树上刚刚查到的位置的下一条记录,发现已经不满足 email='<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="a6dccec7c8c1d5d5dedfdce6dedede88c5c9cb">[email&#160;protected]</a>’的条件了,循环结束。</li>
</ol>
<p>这个过程中,只需要回主键索引取一次数据,所以系统认为只扫描了一行。</p>
<p><strong>如果使用的是 index2</strong>(即 email(6) 索引结构),执行顺序是这样的:</p>
<ol>
<li>从 index2 索引树找到满足索引值是zhangs的记录找到的第一个是 ID1</li>
<li>到主键上查到主键值是 ID1 的行,判断出 email 的值不是’<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="8ef4e6efe0e9fdfdf6f7f4cef6f6f6a0ede1e3">[email&#160;protected]</a>’,这行记录丢弃;</li>
<li>到主键上查到主键值是 ID1 的行,判断出 email 的值不是’<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="c8b2a0a9a6afbbbbb0b1b288b0b0b0e6aba7a5">[email&#160;protected]</a>’,这行记录丢弃;</li>
<li>取 index2 上刚刚查到的位置的下一条记录发现仍然是zhangs取出 ID2再到 ID 索引上取整行然后判断,这次值对了,将这行记录加入结果集;</li>
<li>重复上一步,直到在 idxe2 上取到的值不是zhangs循环结束。</li>
</ol>
@@ -1324,10 +1355,10 @@ from SUser;
<h2>前缀索引对覆盖索引的影响</h2>
<p>前面我们说了使用前缀索引可能会增加扫描行数,这会影响到性能。其实,前缀索引的影响不止如此,我们再看一下另外一个场景。</p>
<p>你先来看看这个 SQL 语句:</p>
<pre><code>select id,email from SUser where email='<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="e59f8d848b8296969d9c9fa59d9d9dcb868a88">[email&#160;protected]</a>';
<pre><code>select id,email from SUser where email='<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="fc86949d929b8f8f848586bc848484d29f9391">[email&#160;protected]</a>';
</code></pre>
<p>与前面例子中的 SQL 语句</p>
<pre><code>select id,name,email from SUser where email='<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="9ce6f4fdf2fbefefe4e5e6dce4e4e4b2fff3f1">[email&#160;protected]</a>';
<pre><code>select id,name,email from SUser where email='<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="1e64767f70796d6d6667645e666666307d7173">[email&#160;protected]</a>';
</code></pre>
<p>相比,这个语句只要求返回 id 和 email 字段。</p>
<p>所以,如果使用 index1即 email 整个字符串的索引结构)的话,可以利用覆盖索引,从 index1 查到结果后直接就返回了,不需要回到 ID 索引再去查一次。而如果使用 index2即 email(6) 索引结构)的话,就不得不回到 ID 索引再去判断 email 字段的值。</p>
@@ -2014,7 +2045,7 @@ SELECT * FROM `information_schema`.`OPTIMIZER_TRACE`\G
/* @b 保存 Innodb_rows_read 的当前值 */
select VARIABLE_VALUE into @b from performance_schema.session_status where variable_name = 'Innodb_rows_read';
/* 计算 Innodb_rows_read 差值 */
select @<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="fb99d6bb9a">[email&#160;protected]</a>;
select @<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="9efcb3deff">[email&#160;protected]</a>;
</code></pre>
<p>这个方法是通过查看 OPTIMIZER_TRACE 的结果来确认的,你可以从 number_of_tmp_files 中看到是否使用了临时文件。</p>
<p><img src="assets/89baf99cdeefe90a22370e1d6f5e6495-1584367392864.png" alt="img" /></p>
@@ -2025,8 +2056,8 @@ select @<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail=
<p>接下来,我再和你解释一下图 4 中其他两个值的意思。</p>
<p>我们的示例表中有 4000 条满足 city='杭州’的记录,所以你可以看到 examined_rows=4000表示参与排序的行数是 4000 行。</p>
<p>sort_mode 里面的 packed_additional_fields 的意思是,排序过程对字符串做了“紧凑”处理。即使 name 字段的定义是 varchar(16),在排序过程中还是要按照实际长度来分配空间的。</p>
<p>同时,最后一个查询语句 select @<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="98fab5d8f9">[email&#160;protected]</a> 的返回结果是 4000表示整个执行过程只扫描了 4000 行。</p>
<p>这里需要注意的是,为了避免对结论造成干扰,我把 internal_tmp_disk_storage_engine 设置成 MyISAM。否则select @<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="67054a2706">[email&#160;protected]</a> 的结果会显示为 4001。</p>
<p>同时,最后一个查询语句 select @<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="dbb9f69bba">[email&#160;protected]</a> 的返回结果是 4000表示整个执行过程只扫描了 4000 行。</p>
<p>这里需要注意的是,为了避免对结论造成干扰,我把 internal_tmp_disk_storage_engine 设置成 MyISAM。否则select @<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="2d4f006d4c">[email&#160;protected]</a> 的结果会显示为 4001。</p>
<p>这是因为查询 OPTIMIZER_TRACE 这个表时,需要用到临时表,而 internal_tmp_disk_storage_engine 的默认值是 InnoDB。如果使用的是 InnoDB 引擎的话,把数据从临时表取出来的时候,会让 Innodb_rows_read 的值加 1。</p>
<h2>rowid 排序</h2>
<p>在上面这个算法过程里面,只对原表的数据读了一遍,剩下的操作都是在 sort_buffer 和临时文件中执行的。但这个算法有一个问题,就是如果查询要返回的字段很多的话,那么 sort_buffer 里面要放的字段数太多,这样内存里能够同时放下的行数很少,要分成很多个临时文件,排序的性能会很差。</p>
@@ -2053,9 +2084,9 @@ select @<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail=
<p>图 5 rowid 排序</p>
<p>对比图 3 的全字段排序流程图你会发现rowid 排序多访问了一次表 t 的主键索引,就是步骤 7。</p>
<p>需要说明的是,最后的“结果集”是一个逻辑概念,实际上 MySQL 服务端从排序后的 sort_buffer 中依次取出 id然后到原表查到 city、name 和 age 这三个字段的结果,不需要在服务端再耗费内存存储结果,是直接返回给客户端的。</p>
<p>根据这个说明过程和图示,你可以想一下,这个时候执行 select @<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="5e3c731e3f">[email&#160;protected]</a>,结果会是多少呢?</p>
<p>根据这个说明过程和图示,你可以想一下,这个时候执行 select @<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="81e3acc1e0">[email&#160;protected]</a>,结果会是多少呢?</p>
<p>现在,我们就来看看结果有什么不同。</p>
<p>首先,图中的 examined_rows 的值还是 4000表示用于排序的数据是 4000 行。但是 select @<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="ee8cc3ae8f">[email&#160;protected]</a> 这个语句的值变成 5000 了。</p>
<p>首先,图中的 examined_rows 的值还是 4000表示用于排序的数据是 4000 行。但是 select @<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="5e3c731e3f">[email&#160;protected]</a> 这个语句的值变成 5000 了。</p>
<p>因为这时候除了排序过程外,在排序完成后,还要根据 id 去原表取值。由于语句是 limit 1000因此会多读 1000 行。</p>
<p><img src="assets/27f164804d1a4689718291be5d10f89b-1584367392865.png" alt="img" /></p>
<p>图 6 rowid 排序的 OPTIMIZER_TRACE 部分输出</p>
@@ -2286,7 +2317,7 @@ SELECT * FROM `information_schema`.`OPTIMIZER_TRACE`\G
</ol>
<p>我们把这个算法,暂时称作随机算法 1。这里我直接给你贴一下执行语句的序列:</p>
<pre><code>mysql&gt; select max(id),min(id) into @M,@N from t ;
set @X= floor((@<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="f2bfdfb2bc">[email&#160;protected]</a>+1)*rand() + @N);
set @X= floor((@<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="216c0c616f">[email&#160;protected]</a>+1)*rand() + @N);
select * from t where id &gt;= @X limit 1;
</code></pre>
<p>这个方法效率很高,因为取 max(id) 和 min(id) 都是不需要扫描索引的,而第三步的 select 也可以用索引快速定位,可以认为就只扫描了 3 行。但实际上,这个算法本身并不严格满足题目的随机要求,因为 ID 中间可能有空洞,因此选择不同行的概率不一样,不是真正的随机。</p>
@@ -6374,7 +6405,7 @@ create table db2.t like db1.t
<p>为了便于说明,我先创建一个用户:</p>
<pre><code>create user 'ua'@'%' identified by 'pa';
</code></pre>
<p>这条语句的逻辑是创建一个用户ua@%’,密码是 pa。注意在 MySQL 里面,用户名 (user)+ 地址 (host) 才表示一个用户,因此 <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="96e3f7d6ffe6a7">[email&#160;protected]</a><a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="6f1a0e2f061f5d">[email&#160;protected]</a> 代表的是两个不同的用户。</p>
<p>这条语句的逻辑是创建一个用户ua@%’,密码是 pa。注意在 MySQL 里面,用户名 (user)+ 地址 (host) 才表示一个用户,因此 <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="d8adb998b1a8e9">[email&#160;protected]</a><a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="047165446d7436">[email&#160;protected]</a> 代表的是两个不同的用户。</p>
<p>这条命令做了两个动作:</p>
<ol>
<li>磁盘上,往 mysql.user 表里插入一行,由于没有指定权限,所以这行数据上所有表示权限的字段的值都是 N</li>
@@ -6580,21 +6611,25 @@ GRANT SELECT(id), INSERT (id,a) ON mydb.mytbl TO 'ua'@'%' with grant option;
<a href="/极客时间/从0开始学微服务.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script><script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709980a94b828b66","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script><script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709ba3afc855fbdc","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
@@ -6608,12 +6643,14 @@ GRANT SELECT(id), INSERT (id,a) ON mydb.mytbl TO 'ua'@'%' with grant option;
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
@@ -6623,5 +6660,7 @@ GRANT SELECT(id), INSERT (id,a) ON mydb.mytbl TO 'ua'@'%' with grant option;
}
return "";
}
</script>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 763 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Some files were not shown because too many files have changed in this diff Show More