<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.9.3">Jekyll</generator><link href="http://www.wcr222.top/feed.xml" rel="self" type="application/atom+xml" /><link href="http://www.wcr222.top/" rel="alternate" type="text/html" /><updated>2024-04-02T20:20:25+08:00</updated><id>http://www.wcr222.top/feed.xml</id><title type="html">个人的技术笔记</title><subtitle>个人的技术笔记官方网站</subtitle><entry><title type="html">JMeter的function扩展——原理解析</title><link href="http://www.wcr222.top/jmeter/2019/07/06/jmeter-custom-function-principle/" rel="alternate" type="text/html" title="JMeter的function扩展——原理解析" /><published>2019-07-06T00:00:00+08:00</published><updated>2019-07-06T00:00:00+08:00</updated><id>http://www.wcr222.top/jmeter/2019/07/06/jmeter-custom-function-principle</id><content type="html" xml:base="http://www.wcr222.top/jmeter/2019/07/06/jmeter-custom-function-principle/"><![CDATA[<h1 id="1-介绍">1. 介绍</h1>

<p>在上一篇中，对JMeter的自定义function功能的使用方法做了介绍。但是众所周知JMeter压测模式是一个典型的多线程并发场景，function代码如何注入，如何实现线程安全等都要从源码中获得答案。</p>

<p>下面对JMeter的function实现原理进行分析</p>

<p>注：源码版本为3.3</p>

<h1 id="2-代码解析">2. 代码解析</h1>

<p>首先看一下Function接口的接口说明，因为AbstractFunction抽象类的注释更清晰，先看下AbstractFunction的源码</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>public abstract class AbstractFunction implements Function {
    /**
     * &lt;p&gt;&lt;b&gt;
     * N.B. execute() should be synchronized if function is operating with non-thread-safe
     * objects (e.g. operates with files).
     * &lt;/b&gt;&lt;/p&gt;
     * JMeter ensures setParameters() happens-before execute(): setParameters is executed in main thread,
     * and worker threads are started after that.
     * @see Function#execute(SampleResult, Sampler)
     */
    @Override
    abstract public String execute(SampleResult previousResult, Sampler currentSampler) throws InvalidVariableException;
	
	.....
</code></pre></div></div>

<p>可以看出，在关键的setParameters、execute接口上，JMeter引擎使setParameters在主线程执行，并保证在execute方法前调用。而execute需要开发者自行保证线程安全，必定运行在多线程环境。</p>

<p>下面先解析一下setParameters如何工作：</p>

<h2 id="21-setparameters方法">2.1 setParameters方法</h2>

<p>先观察入口StandardJMeterEngine.run方法：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    @Override
    public void run() {
        log.info("Running the test!");
        running = true;
		
		......
		
        JMeterContextService.startTest();
        try {
            PreCompiler compiler = new PreCompiler();
            test.traverse(compiler);
        } catch (RuntimeException e) {
		
		......
	}
</code></pre></div></div>

<p>在启动JMeter测试用例时，会使用预处理器(PreCompiler)先对JMeter脚本进行预处理</p>

<p>PreCompiler使用合适的HashTreeTraverser递归处理JMeter脚本（JMeter中称为HashTree），每个节点处理如下：</p>

<p>File: PreCompiler</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@Override
    public void addNode(Object node, HashTree subTree) {
        if(isClientSide) {
			// Server-Client模式下的Client端处理
			......
        } else {
			// 单机模式
            if(node instanceof TestElement) {
                try {
                    replacer.replaceValues((TestElement) node);
                } catch (InvalidVariableException e) {
                    log.error("invalid variables in node {}", ((TestElement)node).getName(), e);
                }
            }
			......
        }
    }
</code></pre></div></div>

<p>上述代码看出，在单机模式与Server-Client模式下，处理方式略有不同，会屏蔽掉一些类型节点。这里我们只关注单机模式下的变量替换逻辑</p>

<p>replacer(ValueReplacer)的replaceValues对${…}变量进行初步处理：</p>

<p>File: ValueReplacer</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>	private Collection&lt;JMeterProperty&gt; replaceValues(PropertyIterator iter, ValueTransformer transform) throws InvalidVariableException {
        List&lt;JMeterProperty&gt; props = new LinkedList&lt;&gt;();
        while (iter.hasNext()) {
            JMeterProperty val = iter.next();
			......
            if (val instanceof StringProperty) {
                // Must not convert TestElement.gui_class etc
                if (!val.getName().equals(TestElement.GUI_CLASS) &amp;&amp;
                        !val.getName().equals(TestElement.TEST_CLASS)) {
                    val = transform.transformValue(val);
                    log.debug("Replacement result: {}", val);
                }
            } else if (val instanceof NumberProperty) {
			.....
			}
		}
		......
	}
</code></pre></div></div>

<p>现在来看一下transform变量（ReplaceStringWithFunctions类）的transformValue方法</p>

<p>File: ReplaceStringWithFunctions</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    @Override
    public JMeterProperty transformValue(JMeterProperty prop) throws InvalidVariableException {
        JMeterProperty newValue = prop;
        getMasterFunction().clear();
        getMasterFunction().setParameters(prop.getStringValue());
        if (getMasterFunction().hasFunction()) {
            newValue = new FunctionProperty(prop.getName(), getMasterFunction().getFunction());
        }
        return newValue;
    }
</code></pre></div></div>

<p>现在到了一个关键的地方，getMasterFunction()实际返回CompoundVariable类实例，使用setParameters方法对表达式解析处理</p>

<p>File: CompoundVariable</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    public void setParameters(String parameters) throws InvalidVariableException {
        this.rawParameters = parameters;
        if (parameters == null || parameters.length() == 0) {
            return;
        }

        compiledComponents = functionParser.compileString(parameters);
        if (compiledComponents.size() &gt; 1 || !(compiledComponents.get(0) instanceof String)) {
            hasFunction = true;
        }
        permanentResults = null; // To be calculated and cached on first execution
        isDynamic = false;
        for (Object item : compiledComponents) {
            if (item instanceof Function || item instanceof SimpleVariable) {
                isDynamic = true;
                break;
            }
        }
    }
</code></pre></div></div>

<p>该处可以看出：</p>

<ul>
  <li>FunctionParser使用compileString方法把字符串表达式再次解析，返回LinkedList<object>类型数据存储表达式内的各型元素</object></li>
  <li>设置hasFunction与isDynamic参数，将会决定上下文迭代时的变量处理行为</li>
</ul>

<p>现在看下compileString的逻辑：</p>

<p>File: FunctionParser</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    LinkedList&lt;Object&gt; compileString(String value) throws InvalidVariableException {
		......
		try {
			while (reader.read(current) == 1) {
				if (current[0] == '\\') { // Handle escapes
					......
				} else if (current[0] == '{' &amp;&amp; previous == '$') {// found "${"
                    buffer.deleteCharAt(buffer.length() - 1);
                    if (buffer.length() &gt; 0) {// save leading text
                        result.add(buffer.toString());
                        buffer.setLength(0);
                    }
                    result.add(makeFunction(reader));
                    previous = ' ';
                }
			}
		} catch (IOException e) {......}
		return result;
	}
</code></pre></div></div>

<p>现在来看一下makeFunction方法</p>

<p>File: FunctionParser</p>
<pre><code class="language-Java">    /**
     * Compile a string into a function or SimpleVariable.
	 ......
	 */
	Object makeFunction(StringReader reader) throws InvalidVariableException {
		......
		while (reader.read(current) == 1) {
			if (current[0] == '\\') {
				......
			} else if (current[0] == '(' &amp;&amp; previous != ' ') {
				String funcName = buffer.toString();
				function = CompoundVariable.getNamedFunction(funcName);
				if (function instanceof Function) {
					((Function) function).setParameters(parseParams(reader));
					......
					return function;
				}
			} else if (.....)
			......
		}
		......
	}
</code></pre>

<p>终于来到这一步，CompoundVariable.getNamedFunction(funcName)将针对不同的functionName构建不同的Function</p>

<p>在下一步中为这个Function实例设置参数值<code class="language-plaintext highlighter-rouge">((Function) function).setParameters(parseParams(reader));</code></p>

<p>而这里的setParameters即我们写的Function扩展的setParameters方法实现</p>

<p>另外可以注意到，这一系列方法栈全都在main函数上运行，印证了开头AbstractFunction的类注释里说的。</p>

<h2 id="22-execute方法">2.2 execute方法</h2>

<p>execute的方法栈从JMeterThread开始，即实际发起请求的线程。通过请求发起相关逻辑（因为发起请求逻辑与各个Sampler类型有关，这里不详述，后面再起专门的主题来讲）</p>

<p>在Sampler处理时，在处理参数时，最终会调用到FunctionProperty的getStringValue方法</p>

<p>File: FunctionProperty</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>	public String getStringValue() {
		......
		if (iter &gt; testIteration || cacheValue == null) {
			testIteration = iter;
			cacheValue = function.execute();
		}
		return cacheValue;
	}
</code></pre></div></div>

<p>这里的function是一个组合的CompoundVariable，并调用execute方法</p>

<p>File: CompoundVariable</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    public String execute(SampleResult previousResult, Sampler currentSampler) {
		......
        StringBuilder results = new StringBuilder();
        for (Object item : compiledComponents) {
            if (item instanceof Function) {
                try {
                    results.append(((Function) item).execute(previousResult, currentSampler));
                } catch (InvalidVariableException e) {
                    // TODO should level be more than debug ?
                    log.debug("Invalid variable: {}", item, e);
                }
            } else if (item instanceof SimpleVariable) {
                results.append(((SimpleVariable) item).toString());
            } else {
                results.append(item);
            }
        }
        if (!isDynamic) {
            permanentResults = results.toString();
        }
        return results.toString();
	}
</code></pre></div></div>

<p>该处方法处理所有compiledComponents（这里大家应该还有印象，在上述PreCompiler中生成过），并通过StringBuilder拼接组合最终的值</p>

<p>这里的<code class="language-plaintext highlighter-rouge">((Function) item).execute(previousResult, currentSampler)</code>中的item即自定义的Function。调用的execute方法即我们的Function扩展的execute方法实现</p>

<p>execute的调用都起于JMeterThread，调用链路上未对execute方法做同步动作，所以开发人员自己保障线程安全</p>

<p>而JMeterThread的生成与运行会在setParameters后</p>

<h1 id="3-总结">3. 总结</h1>

<p>本次代码解析通过setParameters与execute方法的调用分析，印证了AbstractFunction的注释。</p>

<ul>
  <li>
    <p>Function机制的setParameters方法一定会在execute前，运行于JMeter脚本预解析阶段（非常靠前），并且在主线程上运行，不存在线程安全问题</p>
  </li>
  <li>
    <p>Function机制的execute方法运行在多线程环境下，并且内部没有做线程安全处理，需要在开发扩展时特别注意竞态条件并正确处理</p>
  </li>
</ul>]]></content><author><name></name></author><category term="jmeter" /><category term="jmeter,function,原理,custom" /><category term="function" /><summary type="html"><![CDATA[1. 介绍]]></summary></entry><entry><title type="html">JMeter的function扩展——介绍</title><link href="http://www.wcr222.top/jmeter/2019/07/03/jmeter-custom-function-introduction/" rel="alternate" type="text/html" title="JMeter的function扩展——介绍" /><published>2019-07-03T00:00:00+08:00</published><updated>2019-07-03T00:00:00+08:00</updated><id>http://www.wcr222.top/jmeter/2019/07/03/jmeter-custom-function-introduction</id><content type="html" xml:base="http://www.wcr222.top/jmeter/2019/07/03/jmeter-custom-function-introduction/"><![CDATA[<h1 id="1-介绍">1. 介绍</h1>

<p>近期需要在JMeter中集成一些公司业务相关的功能，如加密解密，API签名生成等相关功能。所以想到JMeter的Custom Function（下面简称function）功能来实现。</p>

<p>下面来简单介绍一下JMeter function</p>

<h1 id="2-function扩展开发">2. function扩展开发</h1>

<p>开发一个JMeter的function非常容易，只需要满足三个条件：</p>

<ul>
  <li>
    <p>扩展类，实现Function接口，或继承AbstractFunction</p>
  </li>
  <li>
    <p>扩展类的命名空间需要包含.functions. （可以通过JMeter配置中classfinder.functions.contain项来修改）</p>
  </li>
  <li>
    <p>把编译后的jar包放到JMeter目录lib/ext下，重启JMeter</p>
  </li>
</ul>

<p>想看下是否生效了？可以打开JMeter GUI，在Options-&gt;Function Helper Dialog选项查看所有自定义function</p>

<h1 id="3-des加密函数示例">3. DES加密函数示例</h1>

<p>下面来写一个示例，用于des加密，DesEncryption类自行编写：</p>

<h3 id="31-扩展代码">3.1 扩展代码</h3>

<h3 id="32-代码要点">3.2 代码要点</h3>

<ul>
  <li>
    <p>包管理时需要引入JMeter_core包，scope为provided，版本与最终运行版本一致</p>
  </li>
  <li>
    <p>实现getReferenceKey方法：配置函数名</p>
  </li>
  <li>
    <p>实现getArgumentDesc方法：设置函数参数的说明文字（非必要）</p>
  </li>
  <li>
    <p>实现setParameters方法：从脚本中注册的表达式中获得参数</p>
  </li>
  <li>
    <p>实现execute方法：获得返回值的主体方法，包含主要逻辑</p>
  </li>
</ul>

<h3 id="33-使用方法">3.3 使用方法</h3>

<p>在JMeter中使用表达式直接调用</p>

<p><code class="language-plaintext highlighter-rouge">${__desEncrypt(${content},testDesK)}</code></p>

<ul>
  <li>
    <p>注意不需要加引号</p>
  </li>
  <li>
    <p>参数用逗号分隔，如果参数里也有逗号，需要使用\,转义</p>
  </li>
  <li>
    <p>自定义function支持使用变量，具体实现原理下次再分析</p>
  </li>
</ul>]]></content><author><name></name></author><category term="jmeter" /><category term="jmeter,function" /><summary type="html"><![CDATA[1. 介绍]]></summary></entry><entry><title type="html">JMeter的源码调试</title><link href="http://www.wcr222.top/jmeter/2019/07/01/jmeter-how-to-debug/" rel="alternate" type="text/html" title="JMeter的源码调试" /><published>2019-07-01T00:00:00+08:00</published><updated>2019-07-01T00:00:00+08:00</updated><id>http://www.wcr222.top/jmeter/2019/07/01/jmeter-how-to-debug</id><content type="html" xml:base="http://www.wcr222.top/jmeter/2019/07/01/jmeter-how-to-debug/"><![CDATA[<h1 id="1-背景">1. 背景</h1>

<p>JMeter是一套开源的性能测试工具。因为基于Java语言开发、支持丰富协议、原生支持分布式等特性使JMeter成为非常流行的性能测试工具。</p>

<p>得益于简单的线程策略与长期的开发，JMeter已经相当稳定成熟。所以在性能测试平台上使用了JMeter作为发压引擎。</p>

<p>在对JMeter的使用中，会逐渐产生一些扩展JMeter的需求，从而需要对JMeter的内部机制有一定的了解。然而JMeter使用了Ant这种略古老的版本管理器，所以有了这篇文章，记录调试时的一些经验</p>

<h1 id="2-调试配置">2. 调试配置</h1>

<h2 id="21-准备环境">2.1 准备环境</h2>

<p>下载JMeter源码，地址 <code class="language-plaintext highlighter-rouge">https://github.com/apache/jmeter.git</code></p>

<p>下载安装ant，或者使用IDEA内置ant</p>

<h2 id="22-代码导入">2.2 代码导入</h2>

<p>IDEA打开JMeter源码</p>

<p>使用JDK8，language level设置为8</p>

<p>因为ant项目，导入IDEA时无法自动识别代码路径，需要手动配置：</p>

<p>source目录：</p>

<ul>
  <li>
    <p>src/components</p>
  </li>
  <li>
    <p>src/core</p>
  </li>
  <li>
    <p>src/examples</p>
  </li>
  <li>
    <p>src/functions</p>
  </li>
  <li>
    <p>src/jorphan</p>
  </li>
  <li>
    <p>src/junit</p>
  </li>
  <li>
    <p>src/protocol</p>
  </li>
</ul>

<p>test目录：</p>

<ul>
  <li>test/src</li>
</ul>

<p>excluded目录（排除目录）：</p>

<ul>
  <li>build</li>
</ul>

<h2 id="23-依赖管理">2.3 依赖管理</h2>

<p>打开IDEA的Ant，执行download_jars (即<code class="language-plaintext highlighter-rouge">ant download_jars</code>)</p>

<p>在项目配置中的Libraries添加以下路径：</p>

<ul>
  <li>lib</li>
  <li>lib/doc</li>
  <li>lib/ext</li>
  <li>lib/junit</li>
  <li>lib/opt</li>
</ul>

<h2 id="24-debug配置">2.4 debug配置</h2>

<p>在Run/Debug配置中添加一条Remote Debug，端口假设为5005，得到以下配置<code class="language-plaintext highlighter-rouge">-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005</code></p>

<p>找到build.xml，找到run_gui这个任务，把上述调试指令交入jvm参数，如:</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;jvmarg</span> <span class="na">value=</span><span class="s">"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005"</span><span class="nt">/&gt;</span>
</code></pre></div></div>

<h2 id="25-启动调试">2.5 启动调试</h2>

<p>启动ant run_gui任务，同时打开remote debug，开始调试</p>

<h1 id="总结">总结</h1>

<p>JMeter在IDEA上的调试方式没有在文档上说明，对Ant这种古老的管理工具也不太熟悉，总体来说是摸索出来的。</p>

<p>看来下次得再看看时新的压测工具，如gatling这类较新的工具。后面再对JMeter的一些源码进行分析。</p>]]></content><author><name></name></author><category term="jmeter" /><category term="jmeter,source,debug" /><summary type="html"><![CDATA[1. 背景]]></summary></entry><entry><title type="html">Git的merge与rebase实践</title><link href="http://www.wcr222.top/git/2019/06/27/git-diff-between-merge-and-rebase/" rel="alternate" type="text/html" title="Git的merge与rebase实践" /><published>2019-06-27T00:00:00+08:00</published><updated>2019-06-27T00:00:00+08:00</updated><id>http://www.wcr222.top/git/2019/06/27/git-diff-between-merge-and-rebase</id><content type="html" xml:base="http://www.wcr222.top/git/2019/06/27/git-diff-between-merge-and-rebase/"><![CDATA[<h1 id="1-背景">1. 背景</h1>

<p>网上教程讲git的merge与rebase时，大多讲了两种合并方式的原理。而一般不太讲哪种场景应该使用什么方式</p>

<p>下面通过一些实践来验证与探索这两种方式的使用场景</p>

<p>Git版本2.11.0</p>

<h1 id="2-merge与rebase差异比较">2. Merge与Rebase差异比较</h1>

<p>假设当前git中有两个分支master与test分支，分别提交C、D与E、F如图：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>       E---F (test)
      /
 A---B---C---D (master)
</code></pre></div></div>
<h3 id="21-merge">2.1 merge</h3>

<p>在master分支上执行<code class="language-plaintext highlighter-rouge">git merge test</code>，并进行冲突合并</p>

<h4 id="211-情况1-cdef之间互相冲突">2.1.1 情况1. C、D、E、F之间互相冲突</h4>

<p>合并次数1次，手动解决冲突1次</p>

<p>git log结果为：</p>

<p><code class="language-plaintext highlighter-rouge">A---B---C---D---E---F---G</code></p>

<h4 id="212-情况2-cdef之间不互相冲突">2.1.2 情况2. C、D、E、F之间不互相冲突</h4>

<p>合并次数1次，手动解决冲突0次</p>

<p>git log结果为：</p>

<p><code class="language-plaintext highlighter-rouge">A---B---C---D---E---F---G'</code></p>

<h3 id="22-rebase">2.2 rebase</h3>

<p>在master分支上执行<code class="language-plaintext highlighter-rouge">git rebase test</code>，并进行冲突合并</p>

<h4 id="221-情况1-cdef之间互相冲突直接使用f节点内容忽略cd内容即git-rebase-skip">2.2.1 情况1. C、D、E、F之间互相冲突，直接使用F节点内容，忽略C、D内容（即git rebase –skip）</h4>

<p>合并次数2次，手动解决冲突2次</p>

<p>git log结果为：</p>

<p><code class="language-plaintext highlighter-rouge">A---B---E---F</code></p>

<h4 id="222-情况2-cdef之间互相冲突合并f与cd节点内容">2.2.2 情况2. C、D、E、F之间互相冲突，合并F与C、D节点内容</h4>

<p>合并次数2次，手动解决冲突2次</p>

<p>git log结果为：</p>

<p><code class="language-plaintext highlighter-rouge">A---B---E---F---FC---FCD</code></p>

<h4 id="223-情况3-cdef之间不冲突自动合并f与cd节点">2.2.3 情况3. C、D、E、F之间不冲突，自动合并F与C、D节点</h4>

<p>合并次数2次，手动解决冲突0次</p>

<p>git log结果为：</p>

<p><code class="language-plaintext highlighter-rouge">A---B---E---F---C---D</code></p>

<p>当此时其他人使用<code class="language-plaintext highlighter-rouge">git pull --rebase</code>更新代码时，将把这些节点重新rebase一遍</p>

<h1 id="3-总结">3. 总结</h1>

<p>从上述结果可以看出：</p>

<ul>
  <li>merge在处理合并时，比较的是最终节点内容。不管是否冲突，必然生成一个额外节点（合并节点，即parents=2），并且提交记录包含所有message</li>
  <li>rebase在处理合并时，把基点加进分叉节点后，并一一合并节点内容。冲突情况时，可以选择skip掉某些节点，使skip的节点信息完全被忽略，也可以手动合并，产生新节点；rebase不冲突时不需要新增新节点，版本树清晰</li>
</ul>

<h2 id="对比表格">对比表格</h2>

<hr />

<table>
  <thead>
    <tr>
      <th>类型</th>
      <th>优点</th>
      <th>缺点</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Merge</td>
      <td>冲突时合并相对容易</td>
      <td>产生额外节点污染版本树</td>
    </tr>
    <tr>
      <td>Rebase</td>
      <td>版本树较自然</td>
      <td>在处理多版本冲突时较复杂</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="实践建议">实践建议</h2>

<p>假如大家使用Git多分支开发模式的建议：</p>

<ol>
  <li>
    <p>对于子分支（feature、hotfix类等私有分支）合并入主要分支（release、develop、master等公共分支）时<strong>建议使用merge</strong>，保证在冲突时，能以最终版本为准，减少分支合并工作量与其他人更新时的合并工作量</p>
  </li>
  <li>
    <p>在子分支（feature、hotfix类等私有分支）开发时，合并进更小的孙分支时，<strong>建议使用rebase</strong>保证一个清晰的版本树</p>
  </li>
</ol>]]></content><author><name></name></author><category term="git" /><category term="git,merge,rebase" /><summary type="html"><![CDATA[1. 背景]]></summary></entry><entry><title type="html">基于有向图的测试用例依赖检测</title><link href="http://www.wcr222.top/test/automation/2018/08/09/check-testcase-dependency-by-dag/" rel="alternate" type="text/html" title="基于有向图的测试用例依赖检测" /><published>2018-08-09T00:00:00+08:00</published><updated>2018-08-09T00:00:00+08:00</updated><id>http://www.wcr222.top/test/automation/2018/08/09/check-testcase-dependency-by-dag</id><content type="html" xml:base="http://www.wcr222.top/test/automation/2018/08/09/check-testcase-dependency-by-dag/"><![CDATA[<h1 id="背景">背景</h1>

<p>近期在实施一套测试自动化平台的项目，遇到一个工程问题：平台支持断言、用例参数的动态化，支持从历史用例、前置动作里获取结果，也支持实时运行依赖的用例。这样的功能带来一个问题，如果两个用例之间互相依赖，如A依赖B，B又反过来依赖A，将会导致这个测试集死循环。</p>

<h1 id="思路与基础知识">思路与基础知识</h1>

<p>这是一个典型的依赖检查，可以考虑用图来解决，对应的是有向图（Directed Graph）。</p>

<p>图的定义：是由顶点的有穷非空集合和顶点之间边的集合组成，通常表示为：G(V, E)，其中，G表示一个图，V是图G中顶点的集合，E是图G中边的集合。按照边的有无方向性分为无向图和有向图。</p>

<p>在有向图中，无法从某个顶点出发经过若干条边回到该点，则这个图是一个有向无环图（DAG图），见下图。</p>

<p><img src="/assets/images/1533815166312.png" /></p>

<p>最终我们只要在添加顶点时与边时，时刻检查图是否有环，即可完成依赖检测。</p>

<h1 id="工具库与代码编写">工具库与代码编写</h1>

<p>Google Guava里提供了图相关的库可以使用，所以基于Guava进行开发。代码非常简洁，主要根据TestNG的不同suite，缓存了不同的图。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>public final class TestCaseCycleChecker {
    private static ConcurrentHashMap&lt;String, MutableGraph&lt;Integer&gt;&gt; graphs = new ConcurrentHashMap&lt;&gt;();

    public static void addNode(String suite, Integer testCaseId) {
        getOrCreateGraph(suite).addNode(testCaseId);
    }

    public static void addEdge(String suite, Integer nodeU, Integer nodeV) {
        getOrCreateGraph(suite).putEdge(nodeU, nodeV);
    }

    public static boolean checkCycle(String suite) {
        return Graphs.hasCycle(getOrCreateGraph(suite));
    }

    private static MutableGraph&lt;Integer&gt; getOrCreateGraph(String suite) {
        MutableGraph&lt;Integer&gt; graph = graphs.getOrDefault(suite, null);
        if (null == graph) {
            graph = GraphBuilder
                    .directed()
                    .allowsSelfLoops(true)
                    .build();
            graphs.put(suite, graph);
        }
        return graph;
    }
}
</code></pre></div></div>

<p>无环测试：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>String suite = "uncycled";
TestCaseCycleChecker.addNode(suite, 1);
TestCaseCycleChecker.addNode(suite, 2);
TestCaseCycleChecker.addNode(suite, 3);
TestCaseCycleChecker.addEdge(suite, 1, 2);
TestCaseCycleChecker.addEdge(suite, 2, 3);
TestCaseCycleChecker.addEdge(suite, 1, 3);
Assert.assertTrue(!TestCaseCycleChecker.checkCycle(suite));
</code></pre></div></div>

<p>有环测试：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>String suite = "cycled";
TestCaseCycleChecker.addNode(suite, 1);
TestCaseCycleChecker.addNode(suite, 2);
TestCaseCycleChecker.addNode(suite, 3);
TestCaseCycleChecker.addEdge(suite, 1, 2);
TestCaseCycleChecker.addEdge(suite, 2, 3);
TestCaseCycleChecker.addEdge(suite, 3, 1);
Assert.assertTrue(TestCaseCycleChecker.checkCycle(suite));
</code></pre></div></div>]]></content><author><name></name></author><category term="test" /><category term="automation" /><category term="dag,test" /><category term="automation" /><summary type="html"><![CDATA[背景]]></summary></entry><entry><title type="html">Java8的运行时常量池与MetaSpace</title><link href="http://www.wcr222.top/java/2018/08/03/java-metaspace/" rel="alternate" type="text/html" title="Java8的运行时常量池与MetaSpace" /><published>2018-08-03T00:00:00+08:00</published><updated>2018-08-03T00:00:00+08:00</updated><id>http://www.wcr222.top/java/2018/08/03/java-metaspace</id><content type="html" xml:base="http://www.wcr222.top/java/2018/08/03/java-metaspace/"><![CDATA[<h1 id="起因">起因</h1>

<p>近期在看《深入理解Java虚拟机：JVM高级特性与最佳实践》，在看到运行时常量池这一章，有一个例子演示运行时常量池溢出，代码如下：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/**
 * 原文Java7 VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
 * Java8 VM Args: -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
 */
public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        List&lt;String&gt; list = new ArrayList&lt;String&gt;();
        long i = 0L;
        while(true) {
            list.add(String.valueOf(i++).intern());
        }
    }
}
</code></pre></div></div>

<p>我使用Java8来运行这段程序，自然Java8把PermGen移除，取而代之是Metaspace空间，所以使用下面这个JVM参数来跑。</p>

<p>问题来了，程序跑的结果是过了很久才停止运行，感觉不太对。</p>

<h1 id="原因">原因</h1>

<p>在改JVM参数的时候，考虑的是PermGen迁移到了MetaSpace，那么只需要把参数改成MetaSpace就ok了，然而事实确不是这样。</p>

<p>那么马上我就想到是不是运行时常量池并非从PermGen移到MetaSpace了？既然没有移到MetaSpace，那么很有可能移到Heap区了。</p>

<p>于是经过几次尝试，最后使用<code class="language-plaintext highlighter-rouge">-Xms10m -Xmx10m -XX:-UseGCOverheadLimit</code>重现了这段程序的目的，最后报错信息是<code class="language-plaintext highlighter-rouge">Exception in thread "main" java.lang.OutOfMemoryError: Java heap space</code></p>

<p>移除PermGen原因，见原文<a href="http://openjdk.java.net/jeps/122">JEP 122: Remove the Permanent Generation</a></p>

<p>最终PermGen的方法区移至 Metaspace；字符串常量池移至 Java Heap。</p>]]></content><author><name></name></author><category term="java" /><category term="java" /><summary type="html"><![CDATA[起因]]></summary></entry><entry><title type="html">自组NAS的折腾历程</title><link href="http://www.wcr222.top/nas/2018/05/01/nas-build-and-config/" rel="alternate" type="text/html" title="自组NAS的折腾历程" /><published>2018-05-01T00:00:00+08:00</published><updated>2018-05-01T00:00:00+08:00</updated><id>http://www.wcr222.top/nas/2018/05/01/nas-build-and-config</id><content type="html" xml:base="http://www.wcr222.top/nas/2018/05/01/nas-build-and-config/"><![CDATA[<h1 id="为什么写这篇博客">为什么写这篇博客</h1>

<p>炫技！初衷也没有想做一个大而全的攻略，只想分享个人历程，给其他同学提供一个借鉴思路。</p>

<h3 id="为什么要搭nas">为什么要搭NAS</h3>

<p>自搭NAS的好处懂的同学自然懂，简单来说就是：共享、便捷、安全。</p>

<ul>
  <li>
    <p>备份：平时可以同步备份一些照片、小视频:&gt;、大电影、下载等乱七八糟的东西，对程序猿来说，存一些零碎的配置文件，出门不会丢三落四，不要太爽</p>
  </li>
  <li>
    <p>兼职HTPC：配合一些性能稍好的CPU直接硬解4k，取代专用的HTPC，节省成本</p>
  </li>
  <li>
    <p>兼职服务器：搭一些奇奇怪怪的服务，常见的如gitlab、Nexus、Leanote等，还有小众的服务如Wekan，能用开源方式实现一大堆功能，数据存在自己的服务器上更放心</p>
  </li>
  <li>
    <p>虚拟机服务器：NAS的CPU大多支持虚拟化，能跑一些玩的系统</p>
  </li>
</ul>

<hr />

<h1 id="硬件筹备">硬件筹备</h1>

<p>只打算讲重要的两个：CPU和机箱（毕竟也不拿广告费，哈哈）。</p>

<h3 id="cpu主板">CPU（主板）</h3>

<p>一段时间史：</p>

<ul>
  <li>
    <p>手头有一台老的NAS机器，CPU是D525（Atom），性能比较差，<a href="https://www.cpubenchmark.net/cpu.php?cpu=Intel+Atom+D525+%40+1.80GHz&amp;id=611">CPU Benchmarks</a>，开机比较慢，跑起docker也比较费劲。当HTPC就不要想了。</p>
  </li>
  <li>
    <p>当前比较流行的CPU主要是J3455，<a href="https://www.cpubenchmark.net/cpu.php?cpu=Intel+Celeron+J3455+%40+1.50GHz&amp;id=2875">CPU Benchmarks</a></p>
  </li>
  <li>
    <p>2017年底牙膏厂发布了J4105（可以直接忽略J4205了）,<a href="https://www.cpubenchmark.net/cpu.php?cpu=Intel+Celeron+J4105+%40+1.50GHz&amp;id=3159">CPU Benchmark</a></p>
  </li>
</ul>

<p>J4105优点：</p>

<ul>
  <li>
    <p>原生4k@60hz：与上一代J4205一样，支持到4k@60hz，但是据说hdmi为原生，支持bt.2020，HDR10（没有仔细考究）。</p>
  </li>
  <li>
    <p>支持DDR4：毕竟NAS是个相对低性能的平台，提升内存性能也会有比较高的性价比</p>
  </li>
</ul>

<p>总体来看，J4105在维持低功耗的同时，也能提供相对较好的性能，做HTPC靠谱一点。而且按照电子产品买新不买旧原则，我选择了J4105。</p>

<p>至于主板，截止目前（5月1日）还只听说过华擎出了相应产品（J4105-ITX，翻白眼），这款主板目前来看比较明显的缺点：</p>

<ul>
  <li>
    <p>官方文档说仅支持最高8G内存（4Gx2），未测试</p>
  </li>
  <li>
    <p>板载只有4个SATA口（通过PCI-E x1加扩展卡还能扩展出若干，但是受限于x1的带宽，肯定是存在瓶颈的）</p>
  </li>
  <li>
    <p>主板仅支持UEFI，对linux系统安装会有一些阻碍（实测，可以关闭UEFI Secure，没有遇到啥阻碍）</p>
  </li>
</ul>

<h3 id="机箱">机箱</h3>

<p>选择要点是：</p>

<ul>
  <li>
    <p>静音：不管做什么，静音总是最重要的</p>
  </li>
  <li>
    <p>硬盘位多：别忘记本职</p>
  </li>
  <li>
    <p>颜值高：毕竟要兼职HTPC</p>
  </li>
  <li>
    <p>尽可能小：只能排最后了</p>
  </li>
</ul>

<p>真是组NAS的核心难题。。。</p>

<p>各种机箱之间也差异较大。主要分为以下几类：</p>

<ul>
  <li>
    <p>品牌NAS机箱：专用的热插拔硬盘架，颜值也比较在线，但是性价比不一般的低。没错，说的就是你，万向。PASS</p>
  </li>
  <li>
    <p>定制的工控机箱：价格优势明显，盘位一般也比较多，但是噪音、颜值、体积全下线。PASS</p>
  </li>
  <li>
    <p>HTPC机箱：寻找折中方案，在颜值比较高的HTPC里去寻找盘位尽可能多的产品。</p>
  </li>
</ul>

<p>最后，是在HTPC机箱里找到了一个款比较合适的。。。</p>

<h3 id="组装">组装</h3>

<p><img src="/assets/images/20180502234353.jpg" style="width:100%" alt="主板上电测试" title="主板上电测试" /></p>

<p>主板上电测试（上图）</p>

<p><img src="/assets/images/20180506134248.jpg" style="width:100%" alt="装机测试内部" title="装机测试内部" /></p>

<p>装机测试内部（上图）</p>

<hr />

<h1 id="网络">网络</h1>

<p>网络这块单独拿出来，甚至放在操作系统的前面，是因为后面NAS很多功能都依赖于外部访问，如备份、服务提供。而且通常来说，一个家庭的网络环境通常无法变更，却对后续NAS功能有巨大的影响</p>

<p>下面主要针对如何具体情况给出方案。</p>

<h2 id="家庭网络常见类型">家庭网络常见类型</h2>

<ul>
  <li>
    <p>有公网IP：如ADSL/FTTH(光纤到户)，特点是主路由器上的WAN IP是一个外网地址，可以在外网直接访问到</p>
  </li>
  <li>
    <p>无公网IP：如FTTB(光纤到楼)，特点是主路由器上的WAN IP是一个内网地址，因为NAT，外网无法直接访问</p>
  </li>
</ul>

<h3 id="有公网ipgood-luckman">有公网IP（Good luck，man）</h3>

<p>这种情况一般出现在老小区，路由器PPPoE拨号时分配到一个随机的公网IP，这个IP能在任何地方访问。</p>

<p>我们只需要把NAS机器上的服务端口映射到路由器上，那么服务就连接到了公网。该方式提供的网络速度=网络上行速度（具体要见各地运营商的策略）。唯一需要解决的问题是如果寻找到这个动态的公网IP地址。</p>

<p>注：运营商基本都会禁止访问80/443端口</p>

<p>方案：</p>

<p>绑定域名并且使用dnspod API定期刷新域名解析。<a href="https://github.com/migege/dnspod">参考</a></p>

<h3 id="无公网ipi-am-sorry-for-that">无公网IP（I am sorry for that）</h3>

<p>在新式小区一般才会采用这种方式（运营商成本较低）</p>

<p>这时，一般都需要寻找一台公网服务器，通过端口映射的方式，把本地服务接入外网，常见方案有：</p>

<ul>
  <li>
    <p>花生壳（需要收费服务）</p>
  </li>
  <li>
    <p>私有公网服务器+ssh/frp端口反向映射。</p>
  </li>
</ul>

<p>无论如何，都需要额外的成本，并且注意网络攻击与远程服务器的数据安全。</p>

<p>方案：</p>

<p>弄个国内云主机（挂域名更佳，不过要备案。。），家里使用梅林路由器，在路由器上搭建frp插件，充当内网与公网的桥梁，管理端口映射关系。</p>

<p>当然也可以在NAS机器上直接跑frp，但是在路由器上使用，在多NAS时（未来剁手预定）更容易管理维护</p>

<hr />

<h1 id="操作系统">操作系统</h1>

<p>目前能想到的主流选择是黑群晖、OMV(openmediavault)。</p>

<p>总结一下个人的需求，转化为技术指标的话有以下几点必需要考虑：</p>

<ul>
  <li>
    <p>可靠的数据同步备份方案</p>
  </li>
  <li>
    <p>管理面板远程访问、服务端口映射到外网的实现方案</p>
  </li>
  <li>
    <p>远程下载，能支持普通、BT、电驴等下载</p>
  </li>
  <li>
    <p>docker、虚拟机必须支持</p>
  </li>
</ul>

<p>群晖是集成方案，除了虚拟机无法实现，其他基本都有了比较完善的方案。但是因为黑群晖没有官方服务与技术支持，遇到了一些问题（如突然无法打开docker终端）。所以放弃群晖。</p>

<p>OMV优点：</p>

<ul>
  <li>
    <p>基于debian，对于玩惯了Linux系统的人来说，基本没有实现不了的东西。</p>
  </li>
  <li>
    <p>OMV的整体配置比较底层，能更好的控制系统/组件的运行</p>
  </li>
</ul>

<p>OMV缺点：</p>

<ul>
  <li>每个linux实现不了的功能都需要琢磨一套可行的开源方案</li>
</ul>

<p>总之，OMV更符合我的预期。所以后面NAS会基于OMV。至于群晖，里面有太多兼容性问题，后面就不再讨论了。</p>

<h2 id="omv4安装过程">OMV4安装过程</h2>

<p>OMV提供启动盘下载，<a href="https://www.openmediavault.org/download.html">下载地址</a>
。</p>

<p>但是本人作为资深炫技与轻微技术洁癖人员，仗着熟悉Debian，打算使用debian 9 (Stretch)上升级omv的方式安装。</p>

<p>另外为了安装系统，准备了一个普通的4g U盘作启动盘和一个MLC芯片的32G U盘当作系统盘（正所谓铁打的数据，流水的系统）</p>

<h3 id="启动盘准备">启动盘准备</h3>

<p>写入镜像：插上U盘，刻入Debian Stretch镜像</p>

<h3 id="debian系统安装">Debian系统安装</h3>

<p>插上启动U盘与系统U盘，引导启动U盘，开始安装！</p>

<p><img src="/assets/images/20180506135911.jpg" style="width:100%" alt="开始安装" title="开始安装" /></p>

<p>开始安装</p>

<p><img src="/assets/images/20180506140111.jpg" style="width:100%" alt="注意安装分区" title="注意安装分区" /></p>

<p>安装时盘比较多，需要注意安装到正确的系统U盘上</p>

<p>后面的安装过程不详述，仔细讲的话又可以写一篇文章 :)</p>

<h3 id="omv安装">OMV安装</h3>

<p>现在直接登录到安装好的Debian系统</p>

<p>先加源：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cat &lt;&lt;EOF &gt;&gt; /etc/apt/sources.list.d/openmediavault.list
deb http://packages.openmediavault.org/public arrakis main
EOF
</code></pre></div></div>

<p>安装：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>apt-get update
apt-get --allow-unauthenticated install openmediavault-keyring
apt-get update
apt-get --yes --auto-remove --show-upgraded \
	--allow-downgrades --allow-change-held-packages \
	--no-install-recommends \
	--option Dpkg::Options::="--force-confdef" \
	--option DPkg::Options::="--force-confold" \
	install postfix openmediavault
</code></pre></div></div>

<p>初始化系统：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>omv-initsystem
</code></pre></div></div>

<p>也可以看教程：<a href="https://forum.openmediavault.org/index.php/Thread/21234-Install-OMV4-on-Debian-9-Stretch/">Install OMV4 on Debian 9</a></p>

<p>安装完成后，就可以打开Web管理页面了</p>

<p><img src="/assets/images/20180506142033.png" style="width:100%" alt="注意安装分区" title="注意安装分区" /></p>

<p>登录页面，默认用户密码admin/openmediavault</p>

<h3 id="omv基本配置">OMV基本配置</h3>

<p>插件服务支持</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wget http://omv-extras.org/openmediavault-omvextrasorg_latest_all4.deb
dpkg -i openmediavault-omvextrasorg_latest_all4.deb
</code></pre></div></div>

<h3 id="磁盘分区与共享目录">磁盘分区与共享目录</h3>

<p><img src="/assets/images/20180506151130.png" style="width:100%" alt="物理磁盘" title="物理磁盘" /></p>

<p>确认物理磁盘，并且使用Wipe格式化分区（注意备份数据！！！）</p>

<p><img src="/assets/images/20180506151321.png" style="width:100%" alt="RAID分区" title="RAID分区" /></p>

<p>创建Linux RAID分区，这里因为只有两块盘，采用Mirror镜像（RAID 1）方式</p>

<p><img src="/assets/images/20180506151501.png" style="width:100%" alt="LVM分区" title="LVM分区" /></p>

<p>创建LVM分区，最终创建出需要的逻辑分区。</p>

<p><img src="/assets/images/20180506152510.png" style="width:100%" alt="挂载分区" title="挂载分区" /></p>

<p>挂载分区</p>

<p><img src="/assets/images/20180506152156.png" style="width:100%" alt="共享目录" title="共享目录" /></p>

<p>分别在分区上建立共享目录</p>

<p>可参考的分区：</p>

<ul>
  <li>
    <p>docker-lib：高保护级别。存储docker镜像、容器信息</p>
  </li>
  <li>
    <p>docker-data：高保护级别。存储docker持久化信息</p>
  </li>
  <li>
    <p>virtualbox：高保护级别。因为想玩一下虚拟机，专门划出来存放虚拟机镜像与文件</p>
  </li>
  <li>
    <p>storage：不保护或者低保护级别。主要存放不重要的文档、媒体文件。</p>
  </li>
</ul>

<p>注：因为OMV在服务上挂载共享目录时，会改写目录权限，所以要分别建立共享目录，防止目录权限冲突。</p>

<hr />

<h1 id="常用服务搭建">常用服务搭建</h1>

<h3 id="docker">Docker</h3>

<p><img src="/assets/images/20180506150211.png" style="width:100%" alt="开启Docker源" title="开启Docker源" /></p>

<p>开启Docker源</p>

<p>Plugin菜单开启DOCKER-UI插件</p>

<p><img src="/assets/images/20180506150425.png" style="width:100%" alt="开启Docker源" title="开启Docker源" /></p>

<p>开启Docker组件</p>

<!-- <img src="/assets/images/20180506150837.png" style="width:100%" alt="开启Docker源" title="开启Docker源"> -->

<p>开始使用Docker！</p>

<h3 id="virtualbox">VirtualBox</h3>

<p><img src="/assets/images/20180506153101.png" style="width:100%" alt="开启Docker源" title="开启Docker源" /></p>

<p>很有意思，通过phpvirtualbox在Web管理，创建后可以通过Virtualbox的Remote Display打开远程桌面安装与使用。</p>

<p>主要用于安装Windows系统，干一些linux上干不了的事情。</p>

<p>实际使用体验（Win7，分配1核，2G内存）：略卡。。。好吧，看来J4105也只能勉强带起来</p>

<h3 id="ftp">FTP</h3>

<p><img src="/assets/images/20180506152820.png" style="width:100%" alt="开启Docker源" title="开启Docker源" /></p>

<p>挂载共享目录，就可以让用户通过FTP访问</p>

<h3 id="其他">其他</h3>

<p>其他如NFS、SMB、下载等都是开箱即用，没有什么好说。</p>

<p>并不是苹果粉，Time Machine什么的没研究方案。</p>

<p>下载功能只有基本的，youtube-dl组件也只能挂代理用，实在不行，只能开虚拟Windows解决（无奈脸）</p>

<hr />

<h1 id="总结">总结</h1>

<p>整体搭建下来的感觉是，无休止的折腾，必须要在安全与便捷、定制与易用之间做艰难选择。</p>

<p>目前整体搭建起来后，还比较满意，后面会继续进行NAS应用与HTPC方面的探索。</p>

<hr />

<h1 id="faq">FAQ</h1>

<h3 id="nas和普通pc比优势在哪里">NAS和普通PC比优势在哪里？</h3>

<p>重要的事情说三遍：省电！省电！省电！</p>

<p>实现PC的功能，却只需要20W ~ 30W的电量，一年放着也用不了多少电费（精打细算脸）。</p>

<h3 id="为什么不用白群晖">为什么不用白群晖？</h3>

<p>太贵。。。</p>

<h3 id="磁盘分区方案怎么选择">磁盘分区方案怎么选择？</h3>

<p>目前流行的解决方案:</p>

<ul>
  <li>
    <p>ZFS：基于成熟的Oracle技术，拥有最多的特性，集成RAID技术，应该是首选的方案。虽然因为协议问题没有在Debian的主库中，但是仍然可以使用。</p>
  </li>
  <li>
    <p>LVM2 + Linux RAID：基于Linux维护的软件与库，使用最多的方式。</p>
  </li>
  <li>
    <p>MergerFS + SnapRAID：构建于底层分区体系上的方案，没有实际使用。</p>
  </li>
</ul>

<p>最初通过在OMV上安装ZFS插件，尝试使用ZFS，但是OMV对ZFS支持不佳，应该是设计缺陷，无法在ZFS分区上创建共享目录（Shared Folders），导致无法在GUI上方便使用，于是无奈弃用。。。</p>

<p>关于MergerFS + SnapRAID方案，因为是较新的方案，还未调研功能性与稳定性，不做评论</p>

<p>最后的选择是使用最常见的LVM2 + Linux软RAID。</p>]]></content><author><name></name></author><category term="nas" /><category term="nas" /><category term="htpc" /><summary type="html"><![CDATA[为什么写这篇博客]]></summary></entry><entry><title type="html">Jekyll使用与配置</title><link href="http://www.wcr222.top/jekyll/2015/01/12/jekyll-config-and-usage/" rel="alternate" type="text/html" title="Jekyll使用与配置" /><published>2015-01-12T00:00:00+08:00</published><updated>2015-01-12T00:00:00+08:00</updated><id>http://www.wcr222.top/jekyll/2015/01/12/jekyll-config-and-usage</id><content type="html" xml:base="http://www.wcr222.top/jekyll/2015/01/12/jekyll-config-and-usage/"><![CDATA[<h1 id="前言">前言</h1>

<p>Jekyll 是一个简单的博客形态的静态站点生产机器。它有一个模版目录，其中包含原始文本格式的文档，通过 Markdown （或者 Textile） 以及 Liquid 转化成一个完整的可发布的静态网站，你可以发布在任何你喜爱的服务器上。Jekyll 也可以运行在 GitHub Page 上，也就是说，你可以使用 GitHub 的服务来搭建你的项目页面、博客或者网站，而且是完全免费的。</p>

<p>一些程序员开始在github网站上搭建blog。他们既拥有绝对管理权，又可以享受github带来的便利。不管何时何地，只要向主机提交commit，就能发布新文章。这一切还是免费的，github提供无限流量，世界各地（除了某些地方）都有理想的访问速度。</p>

<h1 id="先从jekyll开始">先从Jekyll开始</h1>

<p>安装Ruby环境与gem工具: Ruby(&gt;=1.9.3)与gem</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">~ <span class="nv">$ </span>gem <span class="nb">install </span>jekyll
~ <span class="nv">$ </span>jekyll new blog
~ <span class="nv">$ </span><span class="nb">cd </span>blog
~/blog <span class="nv">$ </span>bundle <span class="nb">exec </span>jekyll serve</code></pre></figure>

<h1 id="jekyll目录结构">Jekyll目录结构</h1>

<p><em>_config.yml</em></p>

<p>保存配置数据。很多配置选项都可以直接在命令行中进行设置，但是如果你把那些配置写在这儿，你就不用非要去记住那些命令了。</p>

<p><em>_drafts</em></p>

<p>drafts 是未发布的文章。这些文件的格式中都没有 title.MARKUP 数据。学习如何使用 drafts.</p>

<p><em>_includes</em></p>

<p>你可以加载这些包含部分到你的布局或者文章中以方便重用。可以用这个标签  {% include file.ext %} 来把文件 _includes/file.ext 包含进来。</p>

<p><em>_layouts</em></p>

<p>layouts 是包裹在文章外部的模板。布局可以在 YAML 头信息中根据不同文章进行选择。 这将在下一个部分进行介绍。标签  可以将content插入页面中。</p>

<p><em>_posts</em></p>

<p>这里放的就是你的文章了。文件格式很重要，必须要符合: YEAR-MONTH-DAY-title.MARKUP。 The permalinks 可以在文章中自己定制，但是数据和标记语言都是根据文件名来确定的。</p>

<p><em>_site</em></p>

<p>一旦 Jekyll 完成转换，就会将生成的页面放在这里（默认）。最好将这个目录放进你的 .gitignore 文件中。</p>

<p><em>index.html和其他HTML, Markdown, Textile等文件</em></p>

<p>如果这些文件中包含 YAML 头信息 部分，Jekyll 就会自动将它们进行转换。当然，其他的如 .html, .markdown, .md, 或者 .textile 等在你的站点根目录下或者不是以上提到的目录中的文件也会被转换。</p>

<p><em>其他文件</em></p>

<p>其他一些未被提及的目录和文件如  css 还有 images 文件夹， favicon.ico 等文件都将被完全拷贝到生成的 site 中。这里有一些使用 Jekyll 的站点，如果你感兴趣就来看看吧。</p>

<h1 id="全局变量设置">全局变量设置</h1>

<p>编辑_config.yml</p>

<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml"><span class="na">url</span><span class="pi">:</span> <span class="s2">"</span><span class="s">"</span>
<span class="na">keywords</span><span class="pi">:</span> <span class="s">dulio,blog</span>
<span class="c1"># Build settings</span>
<span class="na">paginate</span><span class="pi">:</span> <span class="m">15</span>
<span class="na">paginate_path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">/p/:num"</span></code></pre></figure>

<h1 id="常用托管">常用托管</h1>

<h2 id="托管github-pages--项目主页面">托管Github Pages － 项目主页面</h2>

<p>新建Repo，如blog</p>

<p>把jekyll源码push到gh-pages分支（分支名必须为gh-pages）</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">git init
git add <span class="nt">-f</span> <span class="k">*</span>
git checkout <span class="nt">-b</span> gh-pages
git commit <span class="nt">-m</span> <span class="s1">'first commit'</span>
git remote add https://github.com/dulio/blog.git
git push <span class="nt">-u</span> origin gh-pages</code></pre></figure>

<p>访问dulio.github.io/blog (用户名.github.io/项目名)</p>

<p>也可以绑定自有域名</p>

<h2 id="托管github-pages---用户主页面">托管Github Pages - 用户主页面</h2>
<p>创建名为（用户名.github.io）的项目</p>

<p>推送jekyll代码到master分支下</p>

<h2 id="托管github-pages---用自己的域名">托管Github Pages - 用自己的域名</h2>
<p>项目目录下创建CNAME文件</p>

<p>CNAME文件内容填写域名名称</p>

<h2 id="托管到自有主机">托管到自有主机</h2>
<p>给项目添加Webhooks, Github →项目 → Settings →  Webhooks</p>

<p>主机添加hook脚本</p>

<p>在github push后执行jekyll build操作</p>

<p>配置web server，Jekyll项目下_sites目录</p>]]></content><author><name></name></author><category term="jekyll" /><category term="jekyll" /><category term="github" /><category term="blog" /><summary type="html"><![CDATA[前言]]></summary></entry></feed>