Raccoon Tech Blog [株式会社ラクーン 技術戦略部ブログ]

株式会社ラクーン 技術戦略部より、tipsやノウハウなど技術的な話題を発信いたします。

Goの入門書を書きました

開発の松尾です。
このたび、とある縁に恵まれまして翔泳社よりGoの入門書である『スターティングGo言語』を上梓しました。

12924367_10208745634417646_849801601955218169_n

最近、少しずつですがGoに関連した書籍が増えてきて嬉しい限りですが、「入門書」という体裁の本はあまり見かけないので、これからGoを学びたいというプログラマの方に手にとってもらえると嬉しいですね。2016年初頭にリリースされた最新のGo 1.6に対応しているところがポイントでしょうか。

また、本の発売に合わせて、本日4/21より下記のインタビュー記事がCodeZineに掲載されています。

仕事の言語に飽きてきた人はGoを使ってみてほしい――『スターティングGo言語』著者が語るGoのパワー

このような本を執筆する機会に巡り会えたのは、昨年1月にCodeZineに掲載された記事『EclipseでGoプログラミング! GoClipseのインストールとGojiフレームワークを使ったWeb APIの作成』がきっかけでした。ラクーンのブランディングの一環として外部媒体に記事を出したい、というどちらかと言えば会社の要望があって書いてみた記事ですが、まさかこの記事一本がきっかけになって「本を書いてみませんか」というお誘いを受けるとは思ってもみませんでした。

いただいた提案は嬉しいものの、会社の業務と執筆のバランスがとれるだろうかと悩んでいましたが、自身も2冊の著作経験がある社長・小方功の「業務時間の一部を執筆にあてて良し」という鶴の一声に背中を押されて執筆にとりかかりました。以後、とくに執筆期間の4,5ヶ月の間は「技術書を書くというのはこんなにも大変なのか!」と苦闘の連続でしたが、ラクーンという会社環境の応援もあり何とか走り切ることができました。

※ただ、いくら会社の理解があったとしても業務と執筆の両立は恐ろしく難しかったのは事実です。もっとも筆が進んだのは、間違いなく休日のガストでした(笑)

さて、こんな沿革で書き上げた『スターティングGo言語』ですが、プログラミング経験者が新しくGoを学ぶというコンセプトで構成していますので、「最近ちょっとGoに興味が出てきた」という方にはぴったりなのではないかと自負しています。実は、ラクーンではさほどGoは使っていないのですが(笑)、適したところがあればどんどん活かしていきたいと考えています。

それでは今後のGoの発展を願って~

AWS LambdaとAPI Gatewayを利用し、PagerDutyのインシデント発生時にSlackに専用チャンネルを作成する #1

こんにちは、インフラ及びシステム運用を担当している田中です。

当社ではサーバーやネットワークに障害が発生した際に、障害対応の担当者へ通知を行う手段としてPagerDutyを利用しています。

担当者が障害を認知した後は障害内容を調査し、必要に応じて開発者や責任者へエスカレーションを行うことになります。

障害の発生が平日の日中帯であれば関係者間のコミュニケーションに問題はないのですが、夜間・休日の場合は関係者がそれぞれ別の場所に居ることになるため、障害ごとにSlackの専用チャンネルを作成して関係者間で情報を共有しています。

SlackにはPagerDutyのWebhookを受け、インシデントの内容を特定のチャンネルにpostすることができる機能が用意されています。
しかし、あくまでも特定のチャンネルにpostすることができるだけで、インシデントごとに専用のチャンネルを作成したりすることはできません。

そこで、AWSのAPI Gateway経由でLambdaファンクションを呼び出し、SlackのAPIを叩いて専用チャンネルを作成するようにしています。

今回から数回に分け、この仕組について説明したいと思います。
続きを読む

春なのでSpringへの移行ガイド

こんにちは、開発ユニットbcです。

みなさんご存知かとは思いますが、Seasar2のサポートが2016年9月26日を以って終了します。
弊社の運営するSUPERDELIVERYでも一部の機能でSeasar2を現在も利用しています。

今回はSeasar2からの移行を検討している方の参考になればと思い、以前に行った移行の事例を紹介させていただきます。

2014年にSUPERDELIVERYの商品登録機能をリニューアルした際、弊社ではSeasar2からSpring Framework(4系)へ移行することを選択しました。
選択の理由はプロダクトの開発が停滞していないことと、Seasar2からの移行のしやすさでした。
なぜ移行がしやすいかというと、Seasar2の各機能を代替する機能がSpring Frameworkにも備わっているからです。

ではまずはSeasar2とSpring Frameworkの構成の比較から。

構成の比較

移行前 移行後
Controller S2Struts Spring MVC
DI Seasar2 Spring Framework
DAO S2Dao Spring Data

このようにSeasar2の機能の移行先がSpring Frameworkにもあることが分かります。
では、商品一覧表示の機能を例にしてどのように移行するかを見ていきましょう。

移行前と移行後の実装を比較

移行前のシステムでは、コンポーネント毎に設定ファイルへ記述するスタイルで開発していたので、
新たに機能の追加がある場合は以下のファイルの追加が必要でした。
  1. XXXAction.java
  2. XXXActionImpl.java
  3. XXXService.java
  4. XXXServiceImpl.java
  5. XXXDao.java
  6. XXX.dicon
  7. struts-config-XXX.xml
■ProductSearchAction.java
public interface ProductSearchAction {
  String execute();
}
■ProductSearchActionImpl.java
public class ProductSearchActionImpl implements ProductSearchAction {
  private ProductSearchService productSearchService;
  private List<Product> products;
  private ProductSearchForm searchForm;
	
  public String execute() {
  products = productSearchService.findProducts(searchForm.createProductSearchCondition());
  return "success";
  }

  public List<Product> getProducts() {
  return products;
  }

  public void setProductSearchService(ProductSearchService productSearchService) {
  this.productSearchService = productSearchService;
  }

  public void setSearchForm(ProductSearchForm searchForm) {
  this.searchForm = searchForm;
  }
}
■ProductSearchService.java
public interface ProductSearchService {
List<Product> findProducts(ProductSearchCondition condition);
}
■ProductSearchServiceImpl.java
public class ProductSearchServiceImpl {
private ProductDao productDao;

public List<Product> findProducts(ProductSearchCondition condition) {
return productDao.findByStatusCode(condition.getStatusCode());
}

public void setProductDao(ProductDao productDao) {
this.productDao = productDao;
}
}
■ProductDao.java
public interface ProductDAO {
  @Query("status_code = ?")
  List<Product> findByStatusCode(Integer statusCode);
  void insert(Product product);
  void update(Product product);
  void delete(Product product);
}
■action.dicon
<components>
... <component name="productSearchAction" class="jp.ne.raccoon.action.product.impl.ProductSearchActionImpl" instance="request"> </component>
... </components>
■service.dicon
<components>
  ...
  <component
    class="jp.ne.raccoon.service.product.impl.ProductSearchServiceImpl">
  </component>
  ...
</components>
■dao.dicon
<components>
  ...
  <component
    class="jp.ne.raccoon.dao.ProducDao">
  </component>
  ...
</components>
■struts-config-product.xml
<struts-config>
  <action-mappings>
... <action path="/product/search" type="jp.ne.raccoon.action.product.ProductSearchAction" parameter="execute"> <forward name="success" path="/product/search.html" /> </action>
... </action-mappings> </struts-config>
  1. DIはdiconファイルにコンポーネントを個別に定義して、Seasar2のセッターインジェクションを利用していました。
  2. リクエストのマッピングは、struts-config.xmlにaction-mappingsをaction毎に個別に設定していました。
 
移行後は以下のような実装になりました。
■ProductSearchController.java
@Controller
public class ProductSearchController {
@Autowired private ProductSearchService productSearchService; @RequestMapping(value = "/search", method = RequestMethod.GET) public String execute( @Valid @ModelAttribute("searchForm") ProductSearchForm searchForm, BindingResult bindingResult, Model model) {
model.addAttribute(
"products",
productSearchService.findProducts(searchForm.createProductSearchCondition()));
return "product/search"; } }
■ProductSearchServiceImpl.java (※interfaceは移行前と同じなので割愛してます。)
@Service
public class ProductSearchServiceImpl implements ProductSearchService {
@Autowired private ProductRepository productRepository; public String findProducts(ProductSearchCondition condition) { return productRepository.findByStatusCode(condition.getStatusCode()); } }
■ProductRepository.java
public interface ProductRepository extends JpaRepository<Product, Long> {
  List<Product> findByStatusCode(Integer statusCode);
}

移行前と比べると以下のように実装を置き換えることができました。
Action → Controller
Service → Service
Dao → Repository
diconファイル → @Controller @Service等のアノテーション
struts-config → @RequstMapping
  1. 設定ファイルを必要とせず、Springのアノテーションで コンポーネントの定義とDIを行うようになりました。
  2.  @RequestMappingを利用することでリクエストメソッドの指定やパラメーターの受け取り方等がより柔軟な設定を書けるようになりました。
  3. Daoのinsert/update/deleteのメソッドは、継承元に定義されているのでメソッドを用意する必要はなく、@Queryに相当する物はfindByXXXの部分の命名規則によってwhere句が生成されます。※where status_code = ? が生成されます。
これだけで、新機能の追加に必要な作業がだいぶ軽減されました。
順調に移行できそうでしたが、問題点もありました。

問題点

一つ目が、sqlファイルの外部化です。
S2Daoの資産として、参照系の動的なsqlは外部ファイル化していました。 
Springのプロダクトでは対応している物が見当たらず、可能ならば慣れ親しんだ形で移行したいという思いもあって、Mirageというライブラリを導入することにしました。 

■ProductDao_findProducts.sql
//日付の範囲指定と商品の状態を検索条件で指定できることを例としてます。
select
  product_code
from
  product
/*BEGIN*/ where /*IF dateFrom != null*/ and created_at >= /*dateFrom*/ /*END*/ /*IF dateTo != null*/ and created_at < /*dateTo*/ + 1 /*END*/ /*IF productStatus !=null*/ and product_status = /*productStatus*/
 /*END*/
/*END*/
■ProductDao.java
@Component
@Transactional(readOnly = true)
public class ProductDaoImpl implements ProductDao {
  @Autowired
  private SqlManager sqlManager;

  public Product findProducts(SearchCondition condition) {
//上記sqlファイルを指定 SqlResource sqlResource = new ClasspathSqlResource("ProductDao_find_products.sql"); return sqlManager.getResultList(Product.class, sqlResource, condition); } }
mirageで実装したDaoも@ComponentでSpringのコンポーネントとして定義しました。
必要に応じてspringDataとmirageを使い分けるようにして、移行前の資産も活かすことができました。

二つ目がSeasar2とSpringでのコンポーネントのライフサイクルの違いでした。
S2Strutsの時は、Actionはリクエスト単位で作成していましたが、SpringMVCのControllerでは、デフォルトがシングルトンなため、リクエストやセッション等の短いライフサイクルのコンポーネントのDIができないということでした。
@Controller
@Scope(value = WebApplicationContext.SCOPE_REQUEST) public class XXXController { }
@Scopeの設定でライフサイクルを変えることで対応はできますが、全部のControllerに書くのは・・・ということで、以下のようなアノテーションを作成してライフサイクルをリクエストにしたいControllerに指定するようにしました。
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
@Controller
@Scope(value = WebApplicationContext.SCOPE_REQUEST)
public @interface RequestScopeController {
}
利用する側のController
@RequestScopeController
public class XXXController {
}
これで、リクエストやセッションのライフサイクルのコンポーネントをDIしても問題なく動作するようになりました。

まとめ  

局所的ではありますが、以上がSeasar2から移行した時の内容です。いかがだったでしょうか。
Seasar2のサポート終了をきっかけに新しいことに挑戦していきたいですね。

ローカルUnboundによる名前解決の冗長化の検証


こんにちは!はんだです。
今日はDNSサーバの可用性向上についてのエントリです。

DNSサーバの障害時の影響を防ぐ方法として、
単純にDNSサーバを複数設置して/etc/resolv.confに記入するという方法がありますが、
その方法には、タイムアウトの待ち時間という落とし穴があります。

/etc/resolv.conf
nameserver 192.168.0.11  //DNS1のIPアドレス
nameserver 192.168.0.12  //DNS2のIPアドレス

のように設定したとして、
1行目のDNS1が落ちた状態の場合、2行目のDNS2に移行するまでに、設定次第ですが問い合わせの度に毎回1~数秒のタイムアウトが発生します。名前解決の頻度が高いサービスの場合、この僅かな待ち時間がサービス全体へのパフォーマンスに影響してきます(この問題については[24時間365日]サーバ/インフラを支える技術(技術評論社)の 「DNSサーバの冗長化」の項が詳しいです)。

この問題への対策として、LVS+Keepalivedなどを利用して冗長性を高める方法がありますが、それに加え

複数のDNSサーバをローカルのUnboundの参照先として指定することで冗長性を高める

という方法を検証してみました。

(なお、今回の記事ではLVS構成の設定及び検証については言及しませんので、その部分については他の解説記事などをご参照下さい)

検証環境

検証用の環境として以下を準備しました。
sv-web01       192.168.0.1       Webサーバ。Unboundを導入
sv-dns-lvs01     192.168.0.11       node01とnode02でLVS。Keepalivedで死活監視
  sv-dns-node01  192.168.0.101     BIND
  sv-dns-node02  192.168.0.102     BIND
sv-dns-lvs02     192.168.0.12       node03とnode04でLVS。Keepalivedで死活監視
  sv-dns-node03  192.168.0.103     BIND
  sv-dns-node04  192.168.0.104     BIND

Unbound



sv-web01にはUnboundをインストールし、/etc/resolv.confには以下のようにnameserverを設定します。
nameserver 127.0.0.1       //ローカルのUnbound
nameserver 192.168.0.11    //sv-dns-lvs01
nameserver 192.168.0.12    //sv-dns-lvs02

Unboundの設定

Unboundとは、
  • シンプルな設計になっており高速で動作する。BINDより処理性能が良い(2~3倍)
  • 設定ファイルは1つのみ(unbound.conf)で内容もシンプルなため、設定が容易
  • キャッシュ汚染に対する耐性を強化した設計&DNSSECをサポート
  • デフォルト設定で高いセキュリティが確保された状態で動く
という特徴を持った、新定番となっているDNSキャッシュサーバです。

デフォルトの設定でもキャッシュサーバとして問題なく動作しますが、今回は社内の権威DNSサーバへ問い合わせを行いたいため、stub-zoneの設定を行います。
stub-zoneは、特定のドメインの名前解決について、指定したDNS権威サーバに問い合わせを行うための設定です。

/etc/unbound/unbound.confで、参照先DNSサーバとしてsv-dns-lvs01とsv-dns-lvs02を指定します。
 stub-zone:
        name:"example.com"
        stub-addr:192.168.0.11
        stub-addr:192.168.0.12
stub-zone:
        name: "0.168.192.in-addr.arpa"
        stub-addr:192.168.0.11
        stub-addr:192.168.0.12
Unboundはデフォルトではプライベートアドレスの問い合わせを行わないので、以下の設定を追加します。
private-domain: "example.com"
(中略)
local-zone: "0.168.192.in-addr.arpa" transparent

また、特定のドメインについて、指定したDNSキャッシュサーバに再帰問い合わせを行わせるときにはforward-zoneを設定します。
forward-zoneは、nameオプションに "." を指定することで、全ての問い合わせを転送することもできます。
自社ドメイン以外は別のDNSサーバ(外部DNSなど)を指定して直接問い合わせを行わせるような場合は、こちらで設定します。
forward-zone:
  name: "."
  forward-addr: 203.0.113.111

検証したいこと 

1. stub-zoneに複数DNSサーバを記述した場合、どのような問い合わせ順序になるか。先に書かれているものが優先なのか、ラウンドロビンなのか。

2. stub-zoneに書いたDNSサーバのうち1つが利用不可になった場合に、どのような挙動になるか。

3. 利用不可になった理由が、サーバ自体がダウンした場合と、DNSサービスが利用不可になった場合では、挙動に違いはあるか。resolv.confでは、サーバ自体のダウンではなくDNSサービスが落ちていた場合にタイムアウトまでに長くかかることがあるが、Unboundではどうか。

検証手順

0.事前準備
テスト用のスクリプトを作成します。sv-web01から他のサーバにnslookupを行い、最後にunboundのキャッシュを削除するというだけの単純なものです。
SERVER_LIST="sv-web02 sv-web03 sv-db01 ・・・・・ sv-db20"
while : ; do

    for SERVER in $SERVER_LIST; do
        echo $(date "+%F %H:%M:%S.%N") $(nslookup $SERVER | head -n 1)
    done

    unbound-control -q flush_zone example.com
    sleep 1

done | tee test.log
このスクリプトを実行しながらサーバやサービスを落として、どのサーバで名前解決が行われているかを確認します。
DNSサーバに何の問題も起こっていない時点では以下のようなログになります。
127.0.0.1、つまりローカルのUnboundで名前解決が行われているのがわかります。
test.log

2016-03-23 10:01:47.710318602 Server: 127.0.0.1
2016-03-23 10:01:47.720166753 Server: 127.0.0.1
2016-03-23 10:01:47.730168114 Server: 127.0.0.1
2016-03-23 10:01:47.739930041 Server: 127.0.0.1
2016-03-23 10:01:47.750394444 Server: 127.0.0.1
2016-03-23 10:01:47.760969879 Server: 127.0.0.1
2016-03-23 10:01:47.770984202 Server: 127.0.0.1
2016-03-23 10:01:47.780918869 Server: 127.0.0.1
2016-03-23 10:01:47.791561248 Server: 127.0.0.1
2016-03-23 10:01:47.802000414 Server: 127.0.0.1
2016-03-23 10:01:47.812778410 Server: 127.0.0.1
2016-03-23 10:01:47.823340386 Server: 127.0.0.1
2016-03-23 10:01:47.833941263 Server: 127.0.0.1
2016-03-23 10:01:47.844795349 Server: 127.0.0.1



検証1. 
問い合わせ順序の確認

各LVSサーバでDNSサービスへの接続状態を確認しながら検証用スクリプトを実行してみます。
[root@i-dns-lvs01 ~]# watch ipvsadm -Ln

IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
UDP 192.168.0.11:53 rr
-> 192.168.0.101    Route     0 0 271 
-> 192.168.0.102     Route    0 0 272
[root@i-dns-lvs02 ~]# watch ipvsadm -Ln

IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
UDP 192.168.0.12:53 rr
-> 192.168.0.103    Route     0 0 269 
-> 192.168.0.104     Route    0 0 271 

InActConn(一定時間以内の接続数)がほぼおなじ数字で推移しています。
結論:stub-zoneに複数のDNSサーバを記入した場合、順番に均等に問い合わせが割り振られる 。


検証2.ローカルのUnboundのstub-addrに登録してあるsv-dns-lvs0xのどちらかが落ちた場合


上記スクリプトを実行した状態で、片方のLVS(sv-dns-lvs01)を落としてみます。
[root@sv-dns-lvs01 ~]# service network stop
2016-03-23 11:02:39.654407380 Server: 127.0.0.1
2016-03-23 11:02:39.666387084 Server: 127.0.0.1
2016-03-23 11:02:39.678305469 Server: 127.0.0.1
2016-03-23 11:02:39.690497052 Server: 127.0.0.1
2016-03-23 11:02:39.702769957 Server: 127.0.0.1
2016-03-23 11:02:39.714040967 Server: 127.0.0.1
2016-03-23 11:02:39.725401558 Server: 127.0.0.1
2016-03-23 11:02:39.736123310 Server: 127.0.0.1
2016-03-23 11:02:39.746930229 Server: 127.0.0.1
2016-03-23 11:02:39.758402234 Server: 127.0.0.1
2016-03-23 11:02:39.769120682 Server: 127.0.0.1
2016-03-23 11:02:39.781072673 Server: 127.0.0.1
2016-03-23 11:02:41.296289719 Server: 127.0.0.1
2016-03-23 11:02:41.308759832 Server: 127.0.0.1
2016-03-23 11:02:41.422097623 Server: 127.0.0.1
2016-03-23 11:02:41.434119357 Server: 127.0.0.1
2016-03-23 11:02:42.849200158 Server: 127.0.0.1
2016-03-23 11:02:42.861381999 Server: 127.0.0.1
2016-03-23 11:02:42.873155039 Server: 127.0.0.1
2016-03-23 11:02:42.884901850 Server: 127.0.0.1
2016-03-23 11:02:42.896203445 Server: 127.0.0.1
2016-03-23 11:02:42.906861984 Server: 127.0.0.1
2016-03-23 11:02:42.917331042 Server: 127.0.0.1
2016-03-23 11:02:42.927999125 Server: 127.0.0.1
2016-03-23 11:02:42.938445488 Server: 127.0.0.1
2016-03-23 11:02:42.948876159 Server: 127.0.0.1
2016-03-23 11:02:42.959507049 Server: 127.0.0.1
2016-03-23 11:02:42.970042488 Server: 127.0.0.1

LVSサーバを落としたタイミングで若干処理スピードの低減が発生しましたが、ほぼタイムラグなくsv-dns-lvs01を切り離して引き続きUnboundで名前解決を行ってくれるようです。

結論:stub-addrとして指定した複数のDNSのうち一つが落ちた場合でも、Unbound内で切り離しをして名前解決が続行される。



検証3. sv-dns-lvs0x自体が落ちている場合と、sv-dns-lvs0xにぶらさがっている2つのnodeの両方のDNSサービスが利用不可の場合とで挙動は変わるか


LVS用のサーバが落ちた場合、Unboundがうまく切り離してくれることがわかりました。
では、LVSサーバではなく、node側のDNSサービスが両方落ちた場合はどうでしょうか。

sv-dns-lvs01で利用している、sv-dns-node01とsv-dns-node02の両方で、namedサービスを停止してみます。
[root@sv-dns-node01 ~]# service named stop 
[root@sv-dns-lvs01 ~]# ipvsadm -Ln 

IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
UDP 192.168.0.11:53 rr
-> 192.168.0.101    Route     0 0 0 
-> 192.168.0.102     Route    0 0 0 
2016-03-23 11:26:07.253176732 Server: 127.0.0.1
2016-03-23 11:26:07.263412675 Server: 127.0.0.1
2016-03-23 11:26:07.375477267 Server: 127.0.0.1
2016-03-23 11:26:07.385581955 Server: 127.0.0.1
2016-03-23 11:26:07.395923909 Server: 127.0.0.1
2016-03-23 11:26:07.507452435 Server: 127.0.0.1
2016-03-23 11:26:09.022468808 Server: 127.0.0.1
2016-03-23 11:26:09.635581100 Server: 127.0.0.1
2016-03-23 11:26:09.646748991 Server: 127.0.0.1
2016-03-23 11:26:10.712805036 Server: 127.0.0.1
2016-03-23 11:26:10.725051359 Server: 127.0.0.1
2016-03-23 11:26:10.734853913 Server: 127.0.0.1
2016-03-23 11:26:10.744873105 Server: 127.0.0.1
2016-03-23 11:26:10.754697333 Server: 127.0.0.1
2016-03-23 11:26:10.764829874 Server: 127.0.0.1
2016-03-23 11:26:10.877325395 Server: 127.0.0.1
2016-03-23 11:26:10.887888348 Server: 127.0.0.1
2016-03-23 11:26:10.898024415 Server: 127.0.0.1
2016-03-23 11:26:10.907979399 Server: 127.0.0.1
検証2と同様、若干の処理速度低下はありますが、その後は引き続きUnboundでの名前解決が継続されました。

結論:LVS自体が落ちている場合とサービスが使用不可の場合で挙動は変わらない

まとめ 

検証により、DNSサーバ障害時のパフォーマンスへの影響をローカルUnboundを導入することによりより減らすことができるだろうという結論が得られました。
ここで一度、Unboundを導入することのメリット・デメリットを整理したいと思います。

ローカルにUnboundを導入するメリット
・サーバ障害時も、キャッシュに保存されている分はTTL内(社内サーバなら1時間)であればローカルで名前解決ができる。
・DNSサーバを冗長化しておいた場合に、1台のみの障害であればresolv.confに列記するだけの場合よりもパフォーマンスへの影響を小さくできる。
・ローカルのネットワーク流量やDNSサーバの負荷を抑制できる。
・ローカルで名前解決ができることによるパフォーマンス向上が期待できる。

ローカルにUnboundを導入するデメリット(懸念事項)
・導入コスト(各サーバへの設定)
・運用コスト(local-dataを使う場合はサーバ追加時のlocal-dataの再配布、ドメイン&セグメント追加時のunbound.conf設定修正)
・キャッシュが新しいトラブルの原因になったり、トラブル時の切り分けがしづらくなる可能性
・サーバ負荷軽減やパフォーマンス向上については、名前解決の問い合わせが少ないサーバや、新しい名前解決が多いサーバの場合には、効果が小さい。

これを踏まえ、弊社では、
・TTL内での名前解決が多く発生し、キャッシュにより問い合わせが大きく減らせる
・DNS障害時のタイムラグがサービスのパフォーマンスに影響を与えることが想定できる
など、Unbound導入によるメリットが大きいサーバから順に導入していくことになりました。
また、導入・運用のコストについては、AnsibleやFabricといった自動化ツールを導入することである程度カバーできると考えています。

参考URL

・5分でわかるUnbound http://gihyo.jp/admin/feature/01/unbound/0001
・Unboundサーバ運用Tips http://gihyo.jp/admin/feature/01/unbound/0004
・キャッシュサーバの設定 http://dnsops.jp/event/20140626/cache-config.pdf

Zipファイルを紙とペンで解凍してみた

こんにちは。ほそかわです。

社内勉強会用に、埋めていくだけでzipファイルの解凍ができるプリントを作りました。

普段、zipファイルの解凍はコンピュータにやってもらっていますが、
この記事を読みながらご自身の手を使って紙とペンでやってみましょう。

アルゴリズムとデータ構造を生で感じるチャンスです。

準備

これから使うプリントをダウンロードして印刷しましょう。

「ZIPローカルファイルヘッダ」、
「ZIPセントラルディレクトリファイルヘッダ」、
「ZIPセントラルディレクトリ終端レコード」

記入してZIPの解凍を進めていくプリントです。

「ZIPファイルデータ」
この記事で解凍を行うzipファイルのバイナリデータです。
左側に書いてあるのは16進数で表されたバイナリデータです。主にこちらを使います。
16進数で書かれている行名と列名を足すと何byte目のアドレスにあるデータかが分かります。
右側に書いてあるのは文字列データが簡単に調べられるASCIIで表されたバイナリデータです。

「16進数表」
計算の手間を省くためのものです。

全て印刷して筆記用具も用意できたら準備はOKです。

今回用意したZIPファイルデータは、データ圧縮が行われていない特殊なzipファイルになってます。
圧縮を行わないzipファイルに意味なんてないと感じるかもしれませんが、圧縮に関する仕様は独立しているのでこの記事を読んだ後に別に勉強できます。

無圧縮のzipファイルはzipコマンドでも作れます。

zip -0 file.zip file.txt

プロトコルのバージョンによって違いがあるかもしれないのでこの記事ではこちらで用意したファイルを使います。

概要

Zipファイルの中にはどんなデータが入っているのか見てみましょう。

ファイル全体の概要を図にしました。

zip_overview_multi_file
Zipファイルはヘッダとファイルエントリ、終端レコードで構成されています。

ヘッダには2種類あります。

  • ZIPローカルファイルヘッダ
  • ZIPセントラルディレクトリファイルヘッダ

ZIPセントラルディレクトリファイルヘッダは、ZIPファイルの最後にあるZIPセントラルディレクトリという領域に入っています。そして、ZIPセントラルディレクトリの一番最後にアーカイブ全体についての情報が書いてある終端レコードが付いていてます。

ヘッダにはzipファイルに格納したファイルについてファイル名などの情報が書いてあり、ファイルエントリには格納したファイルの中身が入っています。

図のzipファイルにはA,Bの2つのファイルが格納されています。2種類のヘッダとファイルエントリがファイル数だけ繰り返されています。

ネタバレになってしまいますが繰り返しがあると手間が増えるばかりなので、今回使うzipファイルに格納されているファイルは一つです。

バイナリデータ

Zipファイルは普段扱っているテキストデータよりもコンピュータ向けなバイナリデータとして保存されています。

バイナリデータを扱う為に、16進数、2進数の変換方法とエンディアンについて知っておきましょう。

基礎的な知識なのでWikipediaをみてください。

2進法

16進数

エンディアン

エンディアンは様々なことが書いてありますが、zipファイルを扱うのにはリトルエンディアンとビックエンディアンのデータが読めるようになっていれば十分です。

リトルエンディアンでは1byteをかたまりとして並び順を逆にします。

AB CD EF 12

であれば

12 EF CD AB

となります。

例えば、16進数で1B58という数値をリトルエンディアンで表すと

58 1B

となります。

ビックエンディアンは並び順を変えないもので、

AB CD EF 12

であれば、そのまま

AB CD EF 12

となります。

ZIPセントラルディレクトリ終端レコード

これから紙とペンを使ってzipを解凍していきます。

まずは、アーカイブ全体の情報を得るために、セントラルディレクトリの終端レコードに記録してある情報を調べましょう。

使うプリントは、「ZIPセントラルディレクトリ終端レコード」です。

概要で説明したように、終端レコードはzipファイルの最後の領域にあります。

ZIPの仕様として終端レコードの頭には、

50 4B 05 06

というシグネチャをつけることになっています。

終端レコードはファイルの末尾にあるので末尾から開始地点を表すシグネチャを探すことで、終端レコードの領域を知ることができます。
zipファイルデータの末尾からシグネチャを探しましょう。

アドレス0x5Cに見つかりました。

そこからセントラルディレクトリヘッダの終端レコードが始まってます。
次のアドレスから順番にプリントに書き込んでいくと終端レコードの解凍を進められます。

四角い枠一つが1byteに対応してます。

入力欄解説
中には1byteのデータを書き込みます。

シグネチャは例としてリトルエンディアンで埋めてあります。

次の手順で全て埋めてみてください。

  1.  16進数で読み、数値データはリトルエンディアンで、(文字列)と書いてある文字列のデータはビックエンディアンで四角い枠の欄に記入する。

  2.  数値データは、16進数のデータを10進数に直して下線が引いてある欄に記入する。文字列のデータは1byteごとにASCII文字に変換してある右に表の同じ位置の文字を探して記入する。

(厳密には、文字列のデータもリトルエンディアンになっています。並び替えを行わないビックエンディアンとして扱えている理由は、1文字の1byte単位で保存が行われているからです。リトルエンディアンは1byteをかたまりとして並び替えるものなので、1byteのデータでは並び替えが行われず、リトルエンディアンになる並び替えが行われてないように見えています)

最初なので答えを載せます。

ZIPセントラルディレクトリ終端レコード_答え

全ての記入が終わったら細かく見て行きます。

セントラルディレクトリレコードの合計数

セントラルディレクトリレコードの数はファイルの数と同じになっています。
このzipファイルに含まれるファイルは一つだけだと分かります。

セントラルディレクトリの開始位置のオフセット、セントラルディレクトリのサイズについて

セントラルディレクトリの開始位置のオフセットからは、ファイルの先頭から見てどのアドレスでセントラルディレクトリが始まるのか、セントラルディレクトリのサイズからはどこまで続いているのかが分かります。
セントラルディレクトリは、先頭から39byte目のアドレス0x27で始まって53byte後のアドレス0x5Bで終わっているということがわかります。

Zipファイルコメントと長さについて

このzipファイルにはコメントがないため長さには0が入っています。
コメントは可変長のデータなのでここで長さを定義してそのバイト数だけコメントとして扱うという作りになってます。

その他の項目については、今では使われない機能が多いので割愛します。
興味があったら調べてみてください。

ZIPセントラルディレクトリファイルヘッダ

次は、セントラルディレクトリファイルヘッダです。
使うプリントは、「ZIPセントラルディレクトリファイルヘッダ」です。

終端レコードの情報から、セントラルディレクトリはアドレス0x27で始まっているとわかりました。
セントラルディレクトリファイルヘッダのシグネチャは、

50 4B 01 02

です。

探してみると、終端レコードで書いてあったようにアドレス0x27にあります。

終端レコードの時と同じようにシグネチャの次のバイトからプリントを埋めていきます。

zipでは、日付、時刻のデータがバイト単位で分けられてません。
2byteの領域に3つのデータが入っているので、
一度、2byteを繋げた2進数に変換してから区切り直して、10進数に変換します。

次の手順で行います。

  1. リトルエンディアンで2byte分を2進数に変換して一桁ずつ下線の上に書く
  2. 縦線で区切られているところで分けて2進数から10進数変換して、次の行に書き込む
  3. 秒は2倍、年は1980を足す

このやり方も初めてなので答えを載せておきます。

日時入力欄解説

これでファイルの最終変更日時がわかります。

これも全ての記入が終わったら細かく見ていきます。

圧縮メソッド
ファイルエントリがどのプロトコルで格納されたかが書いてあります。
今回は無圧縮なので0が入っていますが8のDeflateが一般的です。

Wikipedia - Deflate

ファイルの最終変更時刻、最終変更日付
格納したファイルの最終変更日時です。
年は1980の下駄を履かせるので1980年からが使えます。
そして、なんと、秒はビットが足らず2秒刻みで丸められているので2倍します。

zipで圧縮を行ったファイルは最終変更日時の秒が保障されないことをご存知だったでしょうか。いつも使っているものでも意外な発見があるものなんですね。

CRC-32
格納前のファイルエントリで計算したCRC-32が入っています。

圧縮サイズ
格納後のサイズです。ファイルエントリのサイズが書いてあります。

非圧縮サイズ
格納前のファイルサイズです。

ファイル名の長さ、ファイル名
Zipファイルコメントと同じようにまず長さが定義されていて、その長さ分のファイル名データがあります。

ローカルファイルヘッダの相対オフセット
ファイル一個につき、セントラルディレクトリファイルヘッダとローカルファイルヘッダが一つずつあります。

対応するローカルファイルヘッダのアドレスが書いてあります。
最初のファイルのローカルファイルヘッダのアドレスなので0が入っています。

数が多いので他は割愛します。
調べてみてください。

ZIPローカルファイルヘッダとファイルエントリ

ローカルファイルヘッダはZIPセントラルディレクトリファイルヘッダと同じやり方で埋めていけます。
ローカルファイルヘッダの次のアドレスからファイルサイズのバイト数分だけがファイルエントリと呼ばれるファイルの中身のデータになってます。

圧縮が行われている場合には、ヘッダから得た圧縮プロトコル情報をもとにファイルエントリのデコードを行います。今回は無圧縮なので飛ばせます。

ファイルエントリをこれまでヘッダから得た情報をもとにファイルとして保存すればこのファイルは解凍されます。

今回は、

  • ファイルの最終変更日は「2016年1月26日 13時27分36秒」
  • ファイル名は「raccoon」
  • ファイルの中身は「CA FE」

というファイルを作り、ファイルエントリをそのまま中に書き込めばこのzipファイルの解凍は成功です。

「CA FE」とは16進数で使われるA~Fまでの文字という制約で考えた単語「Cafe」です。

最後に

お疲れ様でした!

Zipファイルが身近に感じられるようになったのではないでしょうか。

この記事は最短ルートでzipファイル概要を理解し解凍を体験できることを目標に書いたので多くの仕様を省略しています。
次のステップとして、圧縮アルゴリズムを勉強すると圧縮が行われている普通のzipファイルも解凍できるようになります。他にもパスワード付きや分割など多くの仕様があります。そして、反対のことを行えば圧縮もできます。

ZIPファイルフォーマットは様々なところで使われています。ZIPの知識を生かすことで仕事の幅が広がるかもしれません。このプリントを使って勉強会を開催するのも面白いでしょう。
これからの活躍につながるといいですね。

参考

wikipedia - ZIP

PK-Ware - APPNOTE

記事検索