https://issues.apache.org/jira/browse/HIVE-7027

Field access 를 포함하는 쿼리를 뷰로 만들고 이를 select 했을때 MR 내에서 Operator 들을 초기화 하는 과정에 Expr 이 참조하는 컬럼 이름을 못 찾는 문제가 보고 되었음. 

java.lang.RuntimeException: cannot find field test_c from [0:_col0, 1:_col5]
        at org.apache.hadoop.hive.serde2.objectinspector.ObjectInspectorUtils.getStandardStructFieldRef(ObjectInspectorUtils.java:415)
        at org.apache.hadoop.hive.serde2.objectinspector.StandardStructObjectInspector.getStructFieldRef(StandardStructObjectInspector.java:150)
        at org.apache.hadoop.hive.ql.exec.ExprNodeColumnEvaluator.initialize(ExprNodeColumnEvaluator.java:55)
        at org.apache.hadoop.hive.ql.exec.ExprNodeFieldEvaluator.initialize(ExprNodeFieldEvaluator.java:53)
        at org.apache.hadoop.hive.ql.exec.ExprNodeFieldEvaluator.initialize(ExprNodeFieldEvaluator.java:53)
        at org.apache.hadoop.hive.ql.exec.Operator.initEvaluators(Operator.java:934)
        at org.apache.hadoop.hive.ql.exec.Operator.initEvaluatorsAndReturnStruct(Operator.java:960)

이런 문제는 RowResolver 와 ColExprMap 과 같이 Hive 의 젤 X 같은 코드 + CP/PPD 같은 젤 복잡한 코드가 합작해서 만들어 내는게 보통이라 전혀 쳐다보고 싶지 않았지만, 내가 최근에 커밋한 코드가 CP + limit pushdown 에 관련된 부분이라 도둑이 제발 저리듯 찾아보게 되었다.

두어시간의 삽질 끝에 FieldDesc 의 field (ColumnDesc) 의 alias 가 PPD 과정에 스리 슬쩍 바뀌는 현상을 찾아냄. 알고 보니 PPD 가 Expr mapping 을 과정에 ExprNodeDesc 를 clone 하는데, FieldDesc 의 field 는 clone 하지 않고 그냥 사용해서 생긴 버그였음. 즉,

   public ExprNodeDesc clone() {
-    return new ExprNodeFieldDesc(typeInfo, desc, fieldName, isList);
+    return new ExprNodeFieldDesc(typeInfo, desc.clone(), fieldName, isList);
   }

이게 다임. 끝.

신고
Posted by navis94

https://issues.apache.org/jira/browse/HIVE-7025

하이브를 이용하여 배치처리 작업을 하는 경우 쿼리에 대한 관리성을 높이기 위하여 임시 테이블을 사용하는 경우가 많다. 문제는 이 임시 테이블들이 자동으로 지워지지는 않는다는 점이다. (쿼리 마지막에 drop table 같은 것을 하는데, 쿼리가 중간에 Fail 나면..)

이 패치는 테이블의 생성 시간과 마지막 사용 시간을 검사하여 모두 기준 이상인 테이블에 대해 자동으로 drop table 을 수행하여 준다.

설정 값은 다음과 같다.

hive.metadata.ttl.create.seconds : 테이블 생성 시간(required)

hive.metadata.ttl.access.seconds : 테이블의 마지막 사용 시간(required)

hive.metadata.ttl.database : 임시 테이블이 위치할 데이터베이스 이름(optional)

현재는 모든 테이블에 대해 위의 값을 일괄 적용하도록 되어 있는데, retention 시간을 각 테이블 마다 지정할 수 있도록 하는 것도 고려해 봄직 하다. 파티션 레벨의 retention 정책도 지원하면 좋을 것 같다.

----

너무 위험하다는 의견이 있어서 수정. 사용자가 명시적으로 retention 값을 설정한 테이블에 대해서만 drop 을 수행하도록 함. retention 값을 수정할 수 있는 명령이 없어서 antlr 문법 및 DDL 추가.

alter table srcpart set retention 70 min;

2014-05-21 15:08:20,074 WARN  metastore.ObjectStore (ObjectStore.java:checkTTL(301)) - Dropping partition default.srcpart.ds=2008-04-09/hr=12 by retention policy (Created: Wed May 21 13:58:16 KST 2014, Retention on: 4200 seconds(about 1+ hours)

2014-05-21 15:08:20,074 WARN  metastore.ObjectStore (ObjectStore.java:checkTTL(301)) - Dropping partition default.srcpart.ds=2008-04-09/hr=12 by retention policy (Created: Wed May 21 13:58:16 KST 2014, Retention on: 4200 seconds(about 1+ hours)

2014-05-21 15:08:20,120 WARN  metastore.ObjectStore (ObjectStore.java:checkTTL(301)) - Dropping partition default.srcpart.ds=2008-04-08/hr=11 by retention policy (Created: Wed May 21 13:58:15 KST 2014, Retention on: 4200 seconds(about 1+ hours)

2014-05-21 15:08:20,120 WARN  metastore.ObjectStore (ObjectStore.java:checkTTL(301)) - Dropping partition default.srcpart.ds=2008-04-08/hr=11 by retention policy (Created: Wed May 21 13:58:15 KST 2014, Retention on: 4200 seconds(about 1+ hours)

잘 되는 것 같음.



신고
Posted by navis94

https://issues.apache.org/jira/browse/HIVE-6910

Hive 는 다양한 레벨의 Hook 을 제공하는데, Hook 에 전달되는 HookContext 에는 쿼리에 대한 거의 모든 정보가 들어있다. 그 중에 사용자가 직접 ref 한 컬럼을 알기 위해서는 특별한 설정이 필요한데, hive.stats.collect.scancols=true 로 설정하게 되면 HookContext 의 QueryPlan 에 ColumnAccessInfo 라는 정보를 inflating 해준다 (HIVE-3940). 

일반 테이블의 경우에는 결과가 제대로 나오지만 partitioned 테이블에 대해서는 잘못된 컬럼 값들을 보여주는데, ColumnAccessAnalyzer 에서 사용하는 TS 내의 NeededColumnIDs 라는 정보가 애시당초 partition column 에 대해서는 정보를 제공하지 않기 때문이다. 이는 최초로 ROW 를 만들어 내는 MapOperator 가 Partitioned 테이블에 대해서는 무조건 column 을 생성하도록 되어 있기 때문에 개발자 입장에서는 필요하지 않아서.. 이긴한데, Hook 을 구현하는 사람들을 위해서 제대로 구현하였다. (주로 PartitionPruner 쪽을 수정하였음.)

신고
Posted by navis94

https://issues.apache.org/jira/browse/HIVE-6809

파티션 테이블의 일부 파티션을 삭제하기 위해서는 다음과 같은 명령을 사용한다.

alter table <table> drop partition <partition expression>x n

deep nested 파티션 테이블의 경우 이 expression 은 단순한 partial spec 인 경우가 많은데, 이는 테이블의 상위 파티션 컬럼에 대한 equi-expression 을 의미한다. 예를 들자면 다음과 같다.

alter table srcpart drop partition (ds='2008-04-08')

hive 는 drop expression 에 해당하는 파티션들을 가져온 다음 하나씩 지워나가는 방식을 취하는데, 이는 매 파티션당 세번의 namenode 접근을 포함한다. 일반적인 상황이라면 큰 문제가 없겠지만 부하가 심한 하둡 클러스터의 경우 이 namnode 에 접근하는 작업 자체가 상당한 시간이 걸리게 된다 (hadoop-2 의 경우 namenode 에 개선된 lock mechanism 이 적용되었기 때문에 상황이 좀 낫다). 50 node 로 구성된 busy cluster 에서 1700 개의 파티션을 삭제하는데에 90분 정도 걸린 경우가 있었다.

이 패치는 단순한 partial spec 을 이용한 drop partition 에 대해 최상위 디렉토리를 찾아 한번에 삭제하도록 한 것으로, 예를 들어 아래와 같이 파티션이 구성되어 있다고 하면,

ds=2008-04-08/hr=10/m=00

ds=2008-04-08/hr=10/m=10

ds=2008-04-08/hr=10/m=20

ds=2008-04-08/hr=10/m=30

ds=2008-04-08/hr=10/m=40

ds=2008-04-08/hr=10/m=50

예전의 경우 ds=2008-04-08/hr=10/m=00, ds=2008-04-08/hr=10/m=10 등등을 순서대로 삭제하고 마지막으로 ds=2008-04-08/hr=10 을 삭제하게 되지만, 위의 패치를 적용한 경우 ds=2008-04-08/hr=10 를 한번에 삭제하므로 전체 수행 시간이 줄어들게 된다. 앞서 90분 걸린 쿼리의 경우 패치를 적용한 후 3분 정도에 걸리게 되었다.


신고
Posted by navis94

Java pipilining

2014.03.27 17:46

개삽질 기록을 위해..

shell 상에서 돌아가는 간단한 툴을 많이 만들어 쓰는데, output 이 너무 많을 때는 grep 이나 awk 를 너무 쓰고 싶어진다. 그래서 pipline 이 있는 명령어에 대해 Process 를 만들어 처리한 것 까지는 일사천리였는데, 마지막 4K 정도가 결과가 안나온다. shell 은 tty 가 아닌 경우는 이 정도 크기로 버퍼링을 한다고..  stdbuf -oL -eL <command> 이렇게 하면 된다고 하는데, 영 맘에 안든다. 

결론적으로, 이것땜에 별 지랄을 다 했는데, 왜 진작에 process input 을 close 할 생각을 안했을까 몰라. stream 이 너무 많다보니 헷갈렸나 보다. process input stream 을 close 하니 알아서 flush 되서 잘 나온다. 

> log n_3 | grep SignalCommand | awk '{print $1}' 뭐 이렇게 가능..


신고
Posted by navis94

https://issues.apache.org/jira/browse/HIVE-3050

Hive JDBC 를 이용한 tool 을 작성하는 경우 몇가지 정보가 아쉬울 때가 있다. 대표적인 것이 특정 컬럼이 파티션 컬럼인지 아닌지 여부 같은 것인데, 현재로써는 이를 알 방법이 없다. 물론 desc 명령의 결과를 파싱한다던가 하면 되겠지만, 아웃풋 포맷이 항상 같은 것도 아니고, 파싱하는 것 자체가 상당한 부담이 된다.
위 패치는 파티션 컬럼인지 여부를 JDBC 의 표준 메타 데이터로 제공하는 것으로, 아래와 같이 사용하면 된다.

DatabaseMetaData databaseMetaData = connection.getMetaData();

ResultSet rs = databaseMetaData.getColumns(null, null, "tableName", null);

....

boolean partitionKey = rs.getBoolean("IS_PARTITION_COLUMN");


신고
Posted by navis94

https://issues.apache.org/jira/browse/HIVE-6259

hive 의 table 은 크게 native 와 non-native, managed 와 external 의 두 가지 orthogonal 구분 방식을 기준으로 하여 총 네가지로 구분할 수 있다. non-native 테이블은 hive 의 기본 스토리지 핸들러를 사용하지 않는 테이블을 의미한다. 자세한 설명은 hive wiki 를 참조하도록 한다.

native 테이블의 경우 특정 HDFS 상에 데이터가 존재하기 때문에 전체 내용을 삭제하는 것은 해당 디렉토리를 삭제하고 다시 생성하는 간단한 작업으로 끝난다. 그러나 non-native 테이블, 예를 들어 HBaseStorageHandler 를 사용하는 hbase 테이블의 경우라면 이와 같은 가정이 불가능하다.

현재 hive 는 native 테이블에 대해서만 truncate DDL 을 지원하며(HIVE-446), non-native 테이블에 truncate 명령을 내리는 경우에는 컴파일 과정에 SemanticException 을 발생하도록 되어 있다. 이 패치는 non-native 테이블에 대해서도 truncate 를 명령을 사용할 수 있게 하며,  이를 위해 StorageHandler 에 새로 추가된 아래와 같은 메쏘드를 구현하면 된다.

void truncateTable(org.apache.hadoop.hive.metastore.api.Table table) throws MetaException;

이전과 같이 이 명령을 지원할 수 없는 StorageHandler 는 UnsupportedOperationException 을 throw 하면 된다. hbase 의 경우라면 HBaseAdmin 을 통해 테이블을 지웠다가 새로 생성하는 방법을 쓴다(transactional 하지 않지만 넘어가자). 아래는 nhive 의 HBaseStorageHandler 에 구현된 truncate 메쏘드이다.

@Override

  public void truncateTable(Table table) throws MetaException {

    String tableName = getHBaseTableName(table);

    try {

      HBaseAdmin admin = getHBaseAdmin();

      HTableDescriptor tableDesc = admin.getTableDescriptor(Bytes.toBytes(tableName));

      if (admin.tableExists(tableName)) {

        if (admin.isTableEnabled(tableName)) {

          admin.disableTable(tableName);

        }

        admin.deleteTable(tableName);

        admin.createTable(tableDesc);

      }

    } catch (IOException ie) {

      throw new MetaException(StringUtils.stringifyException(ie));

    }

  }

신고
Posted by navis94

https://issues.apache.org/jira/browse/HIVE-2828

hive-hbase 의 integration 은 hbase storage handler 를 통해 이루어진다. hbase table 과 hive table 의 매핑정보를 serde 프로퍼티로 hbase.columns.mapping 에 지정하면 HBaseSerDe 에서 hbase 의 KeyValue 객체에서 값을 꺼내어 hive 의 row 형태로 만들어 준다. 예를 들면 아래와 같다.

CREATE TABLE hbase_table (key int, value1 string, value2 string)

  STORED BY 'org.apache.hadoop.hive.hbase.HBaseStorageHandler'

  WITH SERDEPROPERTIES ("hbase.columns.mapping" = ":key,cf:value1,cf:value2")

row key 는 :key 라는 예약어에 할당하면 되지만 현재 구현에서는 row 의 timestamp 에 접근할 수 있는 방법이 없다. HIVE-2781 은 hbase table 에 write 할 때 사용할 timestamp 값을 일괄적으로 지정할 수 있게 했지만(hbase.put_timestamp),  단언컨데 별로 유용하지 않다.

이 패치는 row-key 를 :key 라는 예약어에 할당한 것과 마찬가지로 row 의 timestamp 를 :timestamp 라는 예약어에 할당하여 SQL 내에서 사용할 수 있게 해 준다. 예를 들면 아래와 같다.

CREATE TABLE hbase_table (key int, value string, time timestamp)

  STORED BY 'org.apache.hadoop.hive.hbase.HBaseStorageHandler'

  WITH SERDEPROPERTIES ("hbase.columns.mapping" = ":key,cf:string,:timestamp")

위의 경우 사용자는 row key 의 timestamp 를 time 이라는 컬럼으로 사용할 수 있고 write 할때도 필요에 따라 임의의 값을 사용할 수 있게 된다. timestamp 를 여러가지 용도로 사용하는 application 에서는 필수적인 기능이라 할 수 있다.

신고
Posted by navis94
https://issues.apache.org/jira/browse/HIVE-3727 

Staging 이나 table 의 특수한 traits 를 이용하는 MapJoin 이나 SMBJoin 과 달리 driving alias 의 key 에 해당하는 값을 그때 그때 직접 가져 와서 join 을 하는 방식이다. HBase table 과 같은 KeyValue store 에 있는 데이터를 Key 컬럼으로 조인 하는 경우에 적용이 가능하다. 기본적으로 SMBJoin 과 비슷하게 동작하지만 full scan 대신 indexed access 를 이용한다. 

현재 구현은 HINT 를 이용하게 되어 있다. 
select /*+INDEXJOIN(b,c)*/ a.key,b.key+1,c.key+2 from src a join hbase_pushdown b on a.key=b.key join hbase_pushdown c on a.key=c.key 

Plan 은 다음과 같이 나온다.

  TableScan
    alias: a
    Indexed Map Join Operator
      condition map:
        Inner Join 0 to 1
        Inner Join 0 to 2
      condition expressions:
        0 {key}
        1 {key}
        2 {key}
      handleSkewJoin: false
      index reader specs:
        1 org.apache.hadoop.hive.hbase.HBaseIndexedReader:0
        2 org.apache.hadoop.hive.hbase.HBaseIndexedReader:0
      keys:
        0 [class org.apache.hadoop.hive.ql.udf.generic.GenericUDFBridge(Column[key]()]
        1 [Column[key]]
        2 [Column[key]]

1년 6개월을 rebase 한번 안했으니 거의 새로 짜야 할 듯.


신고
Posted by navis94

apache phoenix 가 지원하는 secondary index 를 사용하여 하나의 row key 만 허용하는 hbase storage handler 의 한계를 극복하고자 하는.. POC 요구 사항이 있어서 구현 시작. phoenix 도 hive 와 비슷하게 라이브러리 형태라 설정은 간단한데, hbase 를 새로 설치하느라 조금 애먹었다. 예전에 쓰던 버전은 hive trunk 에 맞춰 0.98.0 인데 phoenix 는 일단 정식 지원 버전이 0.94.16 인지라 그냥 쓰기 찝찝해서 새로 한벌 깔았다. 

가상 분산 모드로 설정하고(hbase.cluster.distributed=true) start-hbase 하니 냅다 IllegalArgumentException: Not a host:port pair: PBUF 라는 괴상한 에러메시지가 나오면서 shutdown 되는데 이것은 다른 버전의 hbase zookeeper data 가 호환이 안되서 나는 문제이니 zookeeper data directory (hbase.zookeeper.property.dataDir) 를 지우던지 적당히 다른 값을 주던지 하면 된다. 

여기에 phoenix 깔고 일단 테이블 생성. Column Family 를 어떤식으로 지정하는지 몰라서 코드 한번 까봄. 아래와 같이 컬럼 이름 앞에 namespace 비슷하게 주면 된다. describe 로 확인 해보니 CF Family 가 생성되어 있다 (phoenix 의 모든 데이블/컬럼은 대문자로 변환된다).

CREATE TABLE hbase_pushdown2(k varchar, cf.v1 varchar, cf.v2 varchar CONSTRAINT my_pk PRIMARY KEY (k)); 

이유는 모르겠는데 UPSERT 가 안된다. update count 는 분명히 > 0 인데, select 해봐도, hbase shell 들어가서 봐도 데이터가 없다. hbase.client.Put 을 찾아 내용을 찍어보면 될 것 같은데, 귀찮아서 넘어감.

UPSERT INTO hbase_pushdown2 VALUES ('foo','bar','bar2');   // returns 1

hbase shell 에서 직접 넣거나, hbase storage handler 를 이용하여 external table 로 phoenix table 을 참조하게 하고, insert select 로 데이터를 부어 주면 phoenix JDBC 에서 보이기는 한다. 대문자를 사용하는 것에 주의.

CREATE external TABLE hbase_pushdown2(k string, v1 string, v2 string) 
  STORED BY 'org.apache.hadoop.hive.hbase.HBaseStorageHandler' 
  WITH SERDEPROPERTIES (
      "hbase.table.name" = "HBASE_PUSHDOWN2", "hbase.columns.mapping" = ":key,CF:V1,CF:V2"); 

INSERT OVERWRITE TABLE hbase_pushdown2 SELECT key, key+100,key+200 FROM src; 

hbase-site.xml 에 hbase.regionserver.wal.codec=org.apache.hadoop.hbase.regionserver.wal.IndexedWALEditCodec 를 설정하고 INDEX 테이블을 만든다(이를 설정 안하면 index 만들때 에러가 난다). INCLUDE 절에 같이 들어갈 컬럼 이름들을 나열하는데 쿼리가 여기에 없는 컬럼을 하나라도 참조하면 그 INDEX 는 이용 못함. 그냥 데이터 두벌 만들어 쓰는 거라고 보면 될 듯. 아래에서는 v1 컬럼을 PK 로 하는 인덱스를 만들었다.

CREATE INDEX hbase_pushdown2_idx ON hbase_pushdown2 (v1) INCLUDE (k,v2); 

그런데 hive-hbase 테이블에 insert 하거나 hbase shell 로 직접 put 하여 대상 테이블에는 데이터가 추가하면 INDEX 테이블이 업데이트가 안된다. ALTER INDEX REBUILD (ALTER INDEX hbase_pushdown2_idx ON hbase_pushdown2 REBUILD) 하면 반영이 되긴 함. 뭔가 옵션이 있는 듯 한데 일단 PASS. 

처음에는 HBase Handler 와 비슷하게 만드려고 했는데, phoenix 라이브러리에 적당한 client API 가 없다. 오직 JDBC 만 지원하는 지라 뭔가 코딩할 여지가 없다. 그렇다고 JDBC Handler 형태로 확장하려니 phoenix 가 서브 쿼리가 안된다 (파싱은 되는데 compile 하다가 에러가 발생). 서브 쿼리도 안되고 당연히 rownum 같은 것도 지원하지 않으니 SQL 자체로는 split 하기가 어렵다. row key 기반으로 hash distribution 을 하자니 서브 쿼리도 안되고 너무 비효율적이라 꺼려진다 (mod 연산은 되나 모르겠다).

Phoenix 소스 코드 반나절 까보다가 아이디어가 떠올라서 구현 시작. 다행히 PhoenixStatement 에 public 으로 compile 메쏘드가 있는데다가 그 결과값에서 Scan object 와 Output 메타데이터를 포함한 전체 Plan 을 참조할 수 있다. 필요한 메쏘드/클래스들이 모두 public 이라 plan 을 내 맘대로 고치고 고친 플랜을 실행하는 것도 가능하다!

일단 Hive 를 고쳐서 subquery pushdown 을 지원하도록 수정. subquery 에 대한 logical planing 직전에 StorageHandler 가 처리할 수 있는지 물어보고, 가능하면 TableScan operator 를 반환 하도록 하고, TS 에 row schema 와 scanspec 을 설정할 수 있도록 하였다. 이 값으로 Hive 가 원하는 대로 RR 과 Table schema 를 새로 만들어 넣고, 사용자가 넘긴 scanspec 을 InputFormat 에서 참조할 수 있도록 수정한다. HiveInputFormat 의 pushdownFilter 하는 부분에서 scan spec 도 JobConf 로 pushdown 하도록 수정. 

TableScanOperator handleSubquery(QB qb, Table table, ASTNode source, TokenRewriteStream rewriter);

PhoenixStorageHandler 에서는 테이블 생성시 사용자가 SerdeParam 으로 지정한 JDBC connection URL 로 PhoenixConnection 를 만들고 subquery 가 컴파일이 가능한지 확인. 이때 phoenix table 과 hive table 의 이름이 다르므로 ASTNode 를 rewrite 해준다. 자동 타입 캐스팅이 지원 되지 않으므로 여기에서 타입 캐스팅도 직접 해야 되는데 todo 로 잡고 일단 PASS. 적당히 TS 를 만든 다음 output 메타데이터를 이용하여 schema 를 생성하고 rewrite 된 query 와 connection url 를 TS 의 scanspec 으로 지정해 준 다음 리턴하면 끝. 

PhoenixDataInputFormat 는 JobConf 에 있는 scanspec 을 꺼내고, compile 하여 만들어진 Scan object 를 TableInputFormat 에 넘겨 region split 을 생성한다. region split 에 scanspec 을 붙여서 반환하면 getSplit() 은 일단 완성. createRecordReader() 는 다시 scanspec 을 꺼내고 compile 하고 scan 에 region split 의 범위를 넣고 execute 한다. ResultSet 과 ResultSetMetadata 를 이미 구현되어 있는 JDBCRecordReader 로 넘기면 작업 끝. 

OutputFormat 은 기존 JDBCOutputFormat 을 그대로 사용하면 됨. 다만 insert 대신 upsert 를 이용하도록 수정.

CREATE external TABLE phoenix1(k string, v1 string, v2 string) 
  STORED BY 'org.apache.hadoop.hive.phoenix.PhoenixStorageHandler' 
  WITH SERDEPROPERTIES (
      "mapred.jdbc.url" = "jdbc:phoenix:localhost:2181", "phoenix.table.name" = "HBASE_PUSHDOWN2"); 

 select k,v1 from phoenix1 where k='4' OR k='400'; 
 select k,v1 from phoenix1 where v1='104.0' OR v1='500.0'; 

두 쿼리 다 full scan 없이 정상적으로 수행 됨. 두번째 쿼리는 hbase_pushdown2_idx 를 타서 들어가는 것도 확인. 그런데 컴파일을 자주 해서 그런지 데이터가 적으면 HBaseHandler 보다 더 느림.

ps.
group by 가 있는 경우는 region split 을 지정하는 것이 아니고 hash distribution 을 사용해야 하는데 이게 가능한지 모르겠다. 일단 diable 해 두었음. order-by 의 경우 서브쿼리라면 그냥 무시하면 되고, 최상위 쿼리라면 그냥 disable 하던가 partial sort 된 파일에 대해 merge fetcher 를 사용해야 하는데, INSERT 면 그것도 힘들 듯.


신고
Posted by navis94

카테고리

분류 전체보기 (31)
Apache Hive (29)

최근에 달린 댓글

최근에 받은 트랙백

태그목록

달력

«   2017/09   »
          1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30

티스토리 툴바