apache zepplin은 ipython notebook(https://ipython.org/ipython-doc/3/notebook/)와 비슷한 툴이지만, 언어에 국한되지 않은 interactive 툴이다. 


hive와 python을 연동하기 위해 0.6.2를 설치한 내용을 공유한다. 



$ wget http://apache.tt.co.kr/zeppelin/zeppelin-0.6.2/zeppelin-0.6.2-bin-all.tgz

$ cp zeppelin-env.sh.template zeppelin-env.sh

$ cp zeppelin-site.xml.template zeppelin-site.xml


$ vi zeppelin-env.sh

// 포트를 따로 지정한다.

export ZEPPELIN_PORT=8888

 

 

$ vi zeppelin-site.xml

 

 <property>

  <name>zeppelin.server.port</name>

  <value>8888</value>

  <description>Server port.</description>

</property>

 


 

여기서 그냥 실행하면 anoymous만 뜨기 때문에 인증을 사용할 수 있도록 수정한다.

자세한 내용은 https://zeppelin.apache.org/docs/0.6.2/security/shiroauthentication.html#2-secure-the-websocket-channel 에 있다. 


$ vi conf/shiro.ini
#/api/version = anon
#/** = anon
/** = authc

shiro.ini 파일에 인증 정보가 있는데 수정한다.

admin = samuel
#admin = password1
#user1 = password2, role1, role2
#user2 = password3, role3
#user3 = password4, role2


$ ./bin/zeppelin-daemon.sh start
(재시작은 restart 커맨드를 사용한다)



웹에서 http://xxx:8888/과 같이 서버에 접근한다. 

create note 해서 하나 샘플로 만들고, python 되는지 확인해본다. 

%python
print "hello world"


apazhe zepplin에는 기본 interpreter가 제공된다. 



그런데, hive Interpreter는 0.6.0과 조금 바뀌었다. hive jdbc url 대신, apache pheonix driver로 교체되었다. 



문서는 쉽게 설명되어 있지만, hive 관련 의존성 라이브러리도 몽땅 가져와야 하기 때문에 일이 크다. 


$ cd interpreter/jdbc
$ cp ~/zeppelin-0.6.0-incubating-SNAPSHOT/interpreter/hive/hive* ~/zeppelin-0.6.2/interpreter/jdbc/



conf/interpreter.json 을 잘 살펴봐야 한다. 

hive 설정은 다음과 같다. 

"2C14JFBSH": {
      "id": "2C14JFBSH",
      "name": "jdbc",
      "group": "jdbc",
      "properties": {
        "phoenix.user": "phoenixuser",
        "hive.url": "jdbc:hive2://localhost:10000",
        "default.driver": "org.postgresql.Driver",
        "phoenix.driver": "org.apache.phoenix.jdbc.PhoenixDriver",
        "hive.user": "hive",
        "psql.password": "",
        "psql.user": "phoenixuser",
        "psql.url": "jdbc:postgresql://localhost:5432/",
        "default.user": "gpadmin",
        "phoenix.hbase.client.retries.number": "1",
        "phoenix.url": "jdbc:phoenix:localhost:2181:/hbase-unsecure",
        "tajo.url": "jdbc:tajo://localhost:26002/default",
        "tajo.driver": "org.apache.tajo.jdbc.TajoDriver",
        "psql.driver": "org.postgresql.Driver",
        "default.password": "",
        "zeppelin.jdbc.concurrent.use": "true",
        "hive.password": "",
        "hive.driver": "org.apache.hive.jdbc.HiveDriver",
        "common.max_count": "1000",
        "phoenix.password": "",
        "zeppelin.jdbc.concurrent.max_connection": "10",
        "default.url": "jdbc:postgresql://localhost:5432/"
      },
      
      
여기서 "hive.url": "jdbc:hive2://localhost:10000" 을 원하는 url로 바꾸고, 
hive.user와 hive.password도 변경하다.


그리고 재시작한다. 
        
$ ./bin/zeppelin-daemon.sh reload


웹에서 http://xxx:8888/과 같이 서버에 접근한다.

노트 하나를 만들어서 테스트해본다. use db스키마 이름 없이  full table을 써야 한다. 

%hive

select * from stat.profile where logdate = 20161016 order by count desc limit 5



Posted by '김용환'
,


hive 버전을 알려면, 다음과 같이 실행한다.



$ hive --version


Hive 1.1.0-cdh5.5.1



또는 libary 명으로 확인한다.


$ ls -al /usr/lib/hive/lib/hive*






hadoop 버전을 알려면, 다음과 같이 실행한다.



$ hadoop version


Hadoop 2.6.0-cdh5.5.1







Posted by '김용환'
,


mongodb 3.2 wiredTiger 버전을 사용 중이고, 대용량 트래픽을 조금씩 받고 있는 mongodb가 있다. 


하지만, mongodb의 replicaset 구축하고 나서, mariadb(mysql) 만큼 안정성이 높지 않은 것에 대해 최근 경악한 일이 있어서 공유한다. 



mongodb가 slave가 RECOVERING 상태가 오래동안 두면, 자동으로 복구가 되길 바라지만 복구가 절대 되지 않는 경우가 있는 것 같다. 그러면 FAILED였으면 더 좋았을 뻔.. 


내가 겪은 내용 뿐 아니라 mongodb는 안전화될 때까지 주기적으로 손을 봐야하고, 안전화가 필요한 것으로 보여진다. (jira 참조)






Replication 상태를 확인해본다. Slave 한대가 Recovering 상태이고, 동기화가 안된지 꽤 오랜 시간이 걸렸다.  slave 자입의 optimeDate가 오래되었다. 동기가 실패된지 꽤 오랜 시간이 되었음을 알린다. 


$ mongo

MongoDB shell version: 3.2.0

connecting to: test

replset:RECOVERING> rs.status()

{

"set" : "replset",

"date" : ISODate("2016-10-21T01:40:13.146Z"),

"myState" : 3,

"term" : NumberLong(-1),

"heartbeatIntervalMillis" : NumberLong(2000),

"members" : [

{

"_id" : 0,

"name" : "1.1.1.1:27017",

"health" : 1,

"state" : 3,

"stateStr" : "RECOVERING",

"uptime" : 25473606,

"optime" : Timestamp(1457316114, 1871),

"optimeDate" : ISODate("2016-03-07T02:01:54Z"),

"maintenanceMode" : 328882,

"infoMessage" : "could not find member to sync from",

"configVersion" : 1,

"self" : true

},

{

"_id" : 1,

"name" : "2.2.2.2:27017",

"health" : 1,

"state" : 1,

"stateStr" : "PRIMARY",

"uptime" : 3521143,

"optime" : Timestamp(1477013949, 1),

"optimeDate" : ISODate("2016-10-21T01:39:09Z"),

"lastHeartbeat" : ISODate("2016-10-21T01:40:12.320Z"),

"lastHeartbeatRecv" : ISODate("2016-10-21T01:40:12.999Z"),

"pingMs" : NumberLong(0),

"electionTime" : Timestamp(1451540357, 1),

"electionDate" : ISODate("2015-12-31T05:39:17Z"),

"configVersion" : 1

},

...





이제 제대로 동기화되게 해보자.



1.1.1.1 (slave)서버에 가서 몽고 셧다운을 한다. 


$ mongod --shutdown

killing process with pid: 26867

$ ps -ef | grep mongo

(없음)




/etc/mongod.conf 에 정의된 db데이터-디렉토리를 백업해두고 새로 디렉토리를 생성한다.


$ mv db데이터-디렉토리  백업디렉토리

$ mkdir db데이터-디렉토리




$  /usr/local/mongodb/bin/mongod -f /etc/mongod.conf


about to fork child process, waiting until server is ready for connections.

forked process: 19670

child process started successfully, parent exiting

$ ps -ef | grep mongo

/usr/local/mongodb/bin/mongod -f /etc/mongod.conf






2.2.2.2(master) 서버에서 replication 상태를 보기 위해 rs.status() 를 실행한다. 1.1.1.1(slave) 서버의 상태가 STARTUP2로 변경되었다. 



$ mongo

MongoDB shell version: 3.2.0

connecting to: test

replset:PRIMARY> rs.status()

{

"set" : "replset",

"date" : ISODate("2016-10-21T01:45:15.535Z"),

"myState" : 1,

"term" : NumberLong(-1),

"heartbeatIntervalMillis" : NumberLong(2000),

"members" : [

{

"_id" : 0,

"name" : "1.1.1.1:27017",

"health" : 1,

"state" : 5,

"stateStr" : "STARTUP2",

"uptime" : 28,

"optime" : Timestamp(0, 0),

"optimeDate" : ISODate("1970-01-01T00:00:00Z"),

"lastHeartbeat" : ISODate("2016-10-21T01:45:15.081Z"),

"lastHeartbeatRecv" : ISODate("2016-10-21T01:45:14.171Z"),

"pingMs" : NumberLong(0),

"lastHeartbeatMessage" : "syncing from: 172.17.58.238:27017",

"syncingTo" : "172.17.58.238:27017",

"configVersion" : 1

},

{

"_id" : 1,

"name" : "2.2.2.2:27017",

"health" : 1,

"state" : 1,

"stateStr" : "PRIMARY",

"uptime" : 25474192,

"optime" : Timestamp(1477014306, 1),

"optimeDate" : ISODate("2016-10-21T01:45:06Z"),

"electionTime" : Timestamp(1451540357, 1),

"electionDate" : ISODate("2015-12-31T05:39:17Z"),

"configVersion" : 1,

"self" : true

},

...






처음 상태는 STARTUP2가 되고, 데이터 동기가 완료되면, SECONDARY로 상태가 변경된다.


데이터가 많을수록, IO(N/W) 시간이 느릴 수록 동기화 완료가 늦어진다.  (이래서 SLAVE 장비가 좋아야 한단 것인가..)




동기화가 완료되면, 제대로 싱크되는지 확인할 수 있다.


replset:SECONDARY> db.printSlaveReplicationInfo()

source: ip:27017

syncedTo: Thu Jan 01 1970 09:00:00 GMT+0900 (KST)

1477015631 secs (410282.12 hrs) behind the primary

source: ip:27017

syncedTo: Fri Oct 21 2016 11:07:11 GMT+0900 (KST)

0 secs (0 hrs) behind the primary




또한, rs.status() 명령어를 실행하면 SECONDARY로 돌아온 것을 확인할 수 있다.





이 문제를 해결하기 위해 다양하게 접근 중이다. 문제 해결을 위해 다양한 해결 방식을 채택하면서 모니터링하면서 살펴봐야할 것 같다. 



1. mongodb 업그레이드

mongodb 3.x는 여진히 진화 중이다. 여전히 버그가 많아서 게속 버전 업을 진행해야 한다. 




2. oplog 크기



몽고DB 문서(https://docs.mongodb.com/manual/core/replica-set-oplog/#replica-set-oplog-sizinghttps://docs.mongodb.com/manual/tutorial/troubleshoot-replica-sets/)에 따르면, oplog 디폴트 크기는 다음과 같다. 

WiredTiger Storage Engine5% of free disk space990 MB50 GB

oplog size를 확인해본다.


$ mongo

MongoDB shell version: 3.2.0

connecting to: test

replset:PRIMARY>  rs.printReplicationInfo()

configured oplog size:   10240MB




설정파일에서 oplog size를 설정할 수 있다. 나는 이미 10G로 내가 임의로 설정할 수 있다. 


replication:

  oplogSizeMB: 10240

  replSetName: "replset"





3. /var/log/mongodb/mongod.log 파일에서 로그 분석


마지막으로 끊긴 시간을 기준으로 로그를 분석해 본다.

"optimeDate" : ISODate("2016-03-07T02:01:54Z"),



해당 일자 단위로 slave 로그를 보면, stale 상태로 된 것을 확인할 수 있다.



[ReplicationExecutor] syncing from: ip:27017

[rsBackgroundSync] we are too stale to use ip:27017 as a sync source

[ReplicationExecutor] could not find member to sync from

[rsBackgroundSync] too stale to catch up -- entering maintenance mode

[rsBackgroundSync] our last optime : (term: -1, timestamp: Mar  7 11:01:54:74f)

[rsBackgroundSync] oldest available is (term: -1, timestamp: Mar  7 11:09:19:21f)

[rsBackgroundSync] See http://dochub.mongodb.org/core/resyncingaverystalereplicasetmember

[ReplicationExecutor] going into maintenance mode with 524 other maintenance mode tasks in progress



로그가 가르키는 문서를 참조해 본다.

https://docs.mongodb.com/manual/tutorial/resync-replica-set-member/



stale 상태가 되면 데이터를 지우고 resync 작업을 진행하고 초기화 작업을 진행한다.

위에서 진행한 작업이 사실상 resync&init 작업이었다. 



  1. Stop the member’s mongod instance. To ensure a clean shutdown, use the db.shutdownServer()method from the mongo shell or on Linux systems, the mongod --shutdown option.
  2. Delete all data and sub-directories from the member’s data directory. By removing the data dbPath, MongoDB will perform a complete resync. Consider making a backup first.





4. 다른 방법


성능이 좋은 장비를 써서 mongodb 쿼리가 빠르게 실행되도록 해야 한다. 

찾아보니,  대용량  map reduce 가 상황에 따라서 blocking을 유발시킬 수 있다고 한다.  관련 내용을 찾아보고 있다. 


http://blog.mlab.com/2013/03/replication-lag-the-facts-of-life/ 글에 따르면 slave가 recovering(stale) 상태에 빠지지 않는 여러 방법을 제안했다. 


1. Make sure your secondary has enough horsepower

2. Consider adjusting your write concern

3. Plan for index builds

4. Take backups without blocking

5. Be sure capped collections have an _id field & a unique index

6. Check for replication errors





5. 툴을 이용한 모니터링


http://www.slideshare.net/revolutionistK/mongo-db-monitoring-mongodb-korea


상용 툴 : mongodb mms

https://www.mongodb.com/cloud


grafana쪽도 살펴봐야 할 것 깉다. 



좀 더 모니터링해보고, 정확한 문제에 대한 해결 방법을 정리해야 할 것 같다. .




6. 다양한 공부



https://www.datadoghq.com/blog/monitoring-mongodb-performance-metrics-wiredtiger/






*****


나중에 해보니.. 


3.4로 업글하고,

replication의 oplogSizeMB을 작게 줄이니(10G->1G) 대용량 트래픽에도 replication이 끊어지지 않게 되었다. 


Posted by '김용환'
,




마리코프 체인는 다음 상태를 결정하기 위해 현재 상태만 의존하는 상태 전이에 대한 통계 모델 방법으로서 


마르코프 체인은 다음 상태를 알기 위해 현재의 상태만 의존한다. 


상태를 따로 저장하지 않기 때문에 메모리가 없어도 된다. 





* 이해를 높일 수 있는 괜찮은 동영상






공부에 도움 되는 자료



* http://electronicsdo.tistory.com/entry/Markov-chain-%EB%A7%88%EC%BD%94%ED%94%84-%EC%B2%B4%EC%9D%B8




* R 로 설명한 마르코프 체인 코드 


http://blog.revolutionanalytics.com/2016/01/getting-started-with-markov-chains.html



Posted by '김용환'
,


자바와 동일하게 스칼라의 정수의 산술 연산은 타입을 따라간다. 




아래와 같은 스칼라 코드는 IllegalFormatConversionException이 발생된다. 

"%1.2f".format(uvValue * 100 / 8423179))


포맷은 float를 원하는데, 실제 값은 Integer이기 때문이다.


java.util.IllegalFormatConversionException: f != java.lang.Integer




따라서, 자바에서처럼 형 변환을 해줘야 한다. 


"%1.2f".format(uvValue.toFloat * 100 / 8423179))


아니면, 변수 선언시 미리 타입을 설정하는 방법도 좋다. 



val uvValue:Float = 30123
println("%1.2f".format(uvValue * 100 / 8423179))



Posted by '김용환'
,

[scala] 특이한 Iterator

scala 2016. 10. 19. 11:35


스칼라의 Iterator는 불변이 아닌 가변(mutable)이다.  마치 포인터처럼 내부 인덱스을 가지고 있기 때문에 잘 알아야 할 필요가 있다. 


object Main extends App {
val list = List(1, 2, 3, 4, 5, 6)
val it = list.iterator
println(it.hasNext)
println(it.next)
println(it.next)
println(it.hasNext)
println(it.size)
println(it.hasNext)
println(it.size)
println(it.hasNext)
}

결과는 다음과 같다.


true

1

2

true

4

false

0

false




size를 호출한 이후, 완전히 이상해져버렸다. 



코드로 설명해본다. ^은 리스트의 인덱스를 가르킨다. 

size를 호출하면, 인덱스가 계산 후 끝으로 가버린다. 남은 크기를 리턴하지. 젙체 크기를 리턴하지 않는다. 


val list = List(1, 2, 3, 4, 5, 6)
val it = list.iterator
println(it.hasNext) // ^ 1 2 3 4 5 6
println(it.next) // 1 ^ 2 3 4 5 6
println(it.next) // 1 2 ^ 3 4 5 6
println(it.hasNext)
println(it.size) // 1 2 3 4 5 6 ^
println(it.hasNext) // false
println(it.size) // 0
println(it.hasNext) // false



문서에 보면, next와 hasNext를 제외하고는 Iterator 내부의 값이 바뀔 수 있다고 한다. 




http://www.scala-lang.org/api/current/index.html#scala.collection.Iterator

It is of particular importance to note that, unless stated otherwise, one should never use an iterator after calling a method on it. The two most important exceptions are also the sole abstract methods: next and hasNext.



Posted by '김용환'
,



Stream 객체를 사용할 때 Stream.empty에 대해서 패턴 매칭을 사용할 수 없다. 



다음 예시를 실행해보면, 에러가 발생한다.

val streamRange = Stream.range(0, 1)
streamRange match {
case Stream.empty => println("empty")
case _ => println("ok")
}



Error:(13, 17) stable identifier required, but scala.`package`.Stream.empty found.

    case Stream.empty => println("empty")





Stream.empty는 메소드이기 때문이다. Stream.empy 메소드의 값을 empty로 받고 패턴 매칭하면 제대로 동작한다.



val streamRange = Stream.range(0, 1)

val empty = Stream.empty
streamRange match {
case empty => println("empty")
case _ => println("ok")
}



결과는 다음과 같다.


empty








Posted by '김용환'
,





한 개의 튜플의 List 를 컬렉션 작업으로 생성했다. 

immutable.Map으로 변환하기 위해서 mutable.HashMap에. toMap을 호출하면 Map으로 생성된다.


//val results = collection 작업.. val tempMap = mutable.HashMap[String, Any]()
for (r <- results) {
tempMap.put(r._1, r._2)
}

tempMap.toMap


Posted by '김용환'
,



triple quotes(""")는 스칼라에서 매우 유용한 기능이다. 

왠만한 것은 다 문자열로 만들 수 있고 멀티 라인 문자열로 만들 수 있다. 자바에서 가장 취약했던 부분을 보완한 느낌이다.



\(역슬래시) 없이 "를 마음껏 쓸 수 있고, json도 편하게 사용할 수 있다.


println("""Hello "Water" World """)

결과는 다음과 같다. 


Hello "Water" World 






처음 봤을 때는 어이없지만, "만 출력하게 할 수 있다.


println(""""""")

결과는 다음과 같다.


"



println("""  {"name":"john"} """)

결과는 다음과 같다.

  {"name":"john"} 



간단한 수식도 사용할 수 있다. 스페이스 * 8 칸..

println(s"hello,${" " * 8}world")

결과는 다음과 같다.

hello,        world



변수도 사용할 수 있다. s를 주면 문자열 바깥의 내용을 참조할 수 있게 한다.


val abc = 111
println("""Hello number ${abc} """)
println(s"""Hello number ${abc} """)

결과는 다음과 같다.


Hello number ${abc} 

Hello number 111 




간단한 수식도 사용할 수 있다. 


val trueFalse = true
println(s"""${if (trueFalse) "true" else "false"} """)


결과는 다음과 같다.


true 





if문과 객체 내부도 접근해서 사용할 수 있다.

case class Comment(val comment_type: String)

val comment = Comment("sticker")
println(s"""${if (comment.comment_type.isEmpty) "X" else comment.comment_type} """)

결과는 다음과 같다.


sticker 




triple quote 앞에 f를 사용하면 문맥에 맞게 출력된다.

println(f"""c:\\user""")
println("""c:\\user""")

결과는 다음과 같다.


c:\user

c:\\user




스칼라는 변수명의 제한이 없는데 특수문자로 변수명으로 사용할 수 있고, 이를 triple quotes에서 사용할 수 있다. 

val `"` = "\""
println(s"${`"`}")

val % = "bbb"
//val `%` = "bbb" // 동일함
println(s"${`%`}")




멀티 라인도 지원한다.

val m =
"""Hi
|This is a String!"""
println(m)

결과는 다음과 같다.


Hi

      |This is a String!




공백 이슈가 있다. 이를 제거하려면 다음을 실행한다.


val s =
"""Hi
|This is a String!""".stripMargin
println(s)


결과는 다음과 같다.


Hi

This is a String!




간단하게 문자열 조작을 stripXXX로 실행할 수 있다. 

val s1 =
"""Hi
|This is a String!""".stripPrefix("Hi").stripSuffix("!").stripMargin
println(s1)

결과는 다음과 같다.



This is a String





"""안에 불필요한 \n\r\b 이런 것들을 몽땅 날리기 위해 StringContext.treatEscapes를 포함시켜 실행한다. 

val s2 =
StringContext.treatEscapes(
"""StringContext.
|treatEscapes!\r\n""".stripMargin)
println(s2)

결과는 다음과 같다.


StringContext.

treatEscapes!





간단히 함수로 묶을 수도 있다. 


def hello(name: String) =
s"""
|Hello, $name
|Come to with your family to World
""".stripMargin

println(hello("samuel"))



결과는 다음과 같다.


Hello, samuel

Come to with your family to World


Posted by '김용환'
,




Spark 1.5, 1.6에서는 json4s는 3.2.x만 쓸 수 있다. 

json4s를 작업하다가 다음과 같은 에러를 만났다. 



org.apache.spark.SparkException: Task not serializable

at org.apache.spark.util.ClosureCleaner$.ensureSerializable(ClosureCleaner.scala:304)

at org.apache.spark.util.ClosureCleaner$.org$apache$spark$util$ClosureCleaner$$clean(ClosureCleaner.scala:294)

at org.apache.spark.util.ClosureCleaner$.clean(ClosureCleaner.scala:122)

at org.apache.spark.SparkContext.clean(SparkContext.scala:2055)

at org.apache.spark.rdd.RDD$$anonfun$flatMap$1.apply(RDD.scala:333)

at org.apache.spark.rdd.RDD$$anonfun$flatMap$1.apply(RDD.scala:332)

at org.apache.spark.rdd.RDDOperationScope$.withScope(RDDOperationScope.scala:150)

at org.apache.spark.rdd.RDDOperationScope$.withScope(RDDOperationScope.scala:111)

at org.apache.spark.rdd.RDD.withScope(RDD.scala:316)

at org.apache.spark.rdd.RDD.flatMap(RDD.scala:332)

at stat.googleStat2$.run(CommentStat2.scala:29)


Caused by: java.io.NotSerializableException: org.json4s.DefaultFormats$

Serialization stack:

- object not serializable (class: org.json4s.DefaultFormats$, value: org.json4s.DefaultFormats$@2b999ee8)

- field (class: stat.CommentStat2$$anonfun$2, name: formats$1, type: class org.json4s.DefaultFormats$)

- object (class stat.CommentStat2$$anonfun$2, <function1>)

at org.apache.spark.serializer.SerializationDebugger$.improveException(SerializationDebugger.scala:40)

at org.apache.spark.serializer.JavaSerializationStream.writeObject(JavaSerializer.scala:47)

at org.apache.spark.serializer.JavaSerializerInstance.serialize(JavaSerializer.scala:101)

at org.apache.spark.util.ClosureCleaner$.ensureSerializable(ClosureCleaner.scala:301)




아래와 같은 형태로 암시 formats를 재활용해서 쓰려 했는데.. 에러가 발생했다.


 implicit val formats = DefaultFormats

 

 

 class MyJob {

  implicit val formats = DefaultFormats


   RDD.map(x => x.extract[Double])

   .filter(y => y.extract[Int] == 18)

}





해결하려면, 다음처럼 formats를 각 메소드에서 선언해서 써야 한다.


class MyJob {

   RDD.map({x =>

     implicit val formats = DefaultFormats

     x.extract[Double]

    })

   ...

   .filter({ y =>

     implicit val formats = DefaultFormats

     y.extract[Int] == 18

    })


}



황당스럽지만, DefaultFormats의 슈퍼 타입인 Formats가 3.3부터 Serialziable을 상속받았다.

(DefaultFormats은 json4s 3.3부터 serialzable을 지원한다. )


trait Formats extends Serializable


https://github.com/json4s/json4s/commit/961fb27f5e69669fddc6bae77079a999fc6f04a1





하지만, Spark 1.5, 1.6을 쓰고 , json4s 3.2를 쓰는 사람 입장에서는 불편하지만 저렇게 써야 한다. 



Posted by '김용환'
,